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


Hello %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." - + emailCLAManagerParams := []emails.ClaManagerInfoParams{} // Build a fancy text string with CLA Manager name as an HTML unordered list for _, manager := range signature.SignatureACL { // 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 @@ -318,29 +403,26 @@ func (s service) sendRequestRejectedEmailToRecipient(companyModel *models.Compan if whichEmail == "" { log.Warnf("unable to send email to manager: %+v - no email on file...", manager) } else { - claManagerText += fmt.Sprintf("
  • %s <%s>
  • ", manager.Username, whichEmail) + emailCLAManagerParams = append(emailCLAManagerParams, emails.ClaManagerInfoParams{ + LfUsername: manager.Username, + Email: whichEmail, + }) } } - claManagerText += "" // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: Approval List Request Denied for Project %s", projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -

    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.

    - -%s -%s`, - recipientName, projectName, - companyName, projectName, projectName, projectName, - requesterName, requesterEmail, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRequestApprovedToCLAManagersTemplate(emailSvc, claGroupModel.Version, claGroupModel.ProjectExternalID, emailParams) + if err != nil { + log.Warnf("rendering email template : %s failed : %v", emails.RequestApprovedToCLAManagersTemplateName, 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 { @@ -860,29 +958,18 @@ list of company’s CLA Managers for %s.

    } } -func sendRequestApprovedEmailToRequester(companyModel *models.Company, claGroupModel *models.ClaGroup, requesterName, requesterEmail string) { - companyName := companyModel.CompanyName +func sendRequestApprovedEmailToRequester(emailSvc emails.EmailTemplateService, emailParams emails.RequestApprovedToRequesterTemplateParams, claGroupModel *models.ClaGroup) { projectName := claGroupModel.ProjectName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: New CLA Manager Access Approved for %s", projectName) - recipients := []string{requesterEmail} - body := fmt.Sprintf(` -

    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.

    - -%s -%s`, - recipientName, projectName, - companyName, projectName, projectName, - requesterName, requesterEmail, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderRequestDeniedToCLAManagersTemplate(emailSvc, claGroupModel.Version, claGroupModel.ProjectExternalID, emailParams) + + if err != nil { + log.Warnf("email template render : %s failed : %v", emails.RequestDeniedToCLAManagersTemplateName, 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 { @@ -920,28 +998,33 @@ be able to maintain the list of employees allowed to contribute to %s on behalf } } -func sendRequestDeniedEmailToRequester(companyModel *models.Company, claGroupModel *models.ClaGroup, requesterName, requesterEmail string) { - companyName := companyModel.CompanyName +func sendRequestDeniedEmailToRequester(emailSvc emails.EmailTemplateService, emailParams emails.CommonEmailParams, claGroupModel *models.ClaGroup) { projectName := claGroupModel.ProjectName // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: New CLA Manager Access Denied for %s", projectName) - recipients := []string{requesterEmail} - body := fmt.Sprintf(` -

    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.

    - -%s -%s`, - recipientName, projectName, - companyName, projectName, projectName, projectName, - name, email, - utils.GetEmailHelpContent(claGroupModel.Version == utils.V2), utils.GetEmailSignOffContent()) - - err := utils.SendEmail(subject, body, recipients) + recipients := []string{emailParams.RecipientAddress} + body, err := emails.RenderClaManagerAddedToCLAManagersTemplate(emailSvc, claGroupModel.Version, claGroupModel.ProjectExternalID, emailParams) + if err != nil { + log.Warnf("email template render : %s failed : %v", emails.ClaManagerAddedToCLAManagersTemplate, 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 { @@ -411,12 +502,11 @@ list of company’s CLA Managers for %s.

    } // sendRequestRejectedEmailToRecipient generates and sends an email to the specified recipient -func sendRemovedClaManagerEmailToRecipient(companyModel *models.Company, claGroupModel *models.ClaGroup, recipientName, recipientAddress string, claManagers []models.User) { - companyName := companyModel.CompanyName +func sendRemovedClaManagerEmailToRecipient(emailSvc emails.EmailTemplateService, emailParams emails.CommonEmailParams, claGroupModel *models.ClaGroup, claManagers []models.User) { projectName := claGroupModel.ProjectName - var companyManagerText = "" - companyManagerText += "" - // subject string, body string, recipients []string subject := fmt.Sprintf("EasyCLA: Removed as CLA Manager for Project %s", projectName) - recipients := []string{recipientAddress} - body := fmt.Sprintf(` -

    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}}:

    + +` +) + +// RenderApprovalListRejectedTemplate renders RequestToAuthorizeTemplate +func RenderApprovalListRejectedTemplate(svc EmailTemplateService, claGroupVersion string, projectSFID string, params ApprovalListRejectedTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupVersion, projectSFID) + if err != nil { + return "", err + } + + // assign the prefilled struct + params.CLAGroupTemplateParams = claGroupParams + return RenderTemplate(claGroupVersion, ApprovalListRejectedTemplateName, ApprovalListRejectedTemplate, + params, + ) + +} + +// ApprovalListApprovedTemplateParams is email params for Approval +type ApprovalListApprovedTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + Approver string +} + +const ( + // ApprovalListApprovedTemplateName is email template name for ApprovalListRejectedTemplate + ApprovalListApprovedTemplateName = "ApprovalListApprovedTemplate" + // ApprovalListApprovedTemplate is email template for + ApprovalListApprovedTemplate = ` +

    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}}


    +{{end}} +

    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, "
  • LFUserName LFEmail
  • ") +} + +func TestApprovalListApprovedTemplate(t *testing.T) { + params := ApprovalListApprovedTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "Recipient", + CompanyName: "CompanyFoo", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + CLAGroupName: "CLAGroupFoo", + Projects: []CLAProjectParams{ + {ExternalProjectName: "Project1", ProjectSFID: "ProjectSFID1", FoundationSFID: "FoundationSFID1", CorporateConsole: "http://CorporateConsole.com"}, + {ExternalProjectName: "Project2", ProjectSFID: "ProjectSFID2", FoundationSFID: "FoundationSFID2", CorporateConsole: "http://CorporateConsole.com"}, + }, + }, + Approver: "LFUsername", + } + + result, err := RenderTemplate(utils.V2, ApprovalListApprovedTemplateName, ApprovalListApprovedTemplate, params) + + assert.NoError(t, err) + assert.Contains(t, result, "Hello Recipient") + assert.Contains(t, result, "regarding the CLA Group CLAGroupFoo") + assert.Contains(t, result, "You have been added to the Approval list of CompanyFoo for CLAGroupFoo by CLA Manager LFUsername.") + assert.Contains(t, result, "This means that you are authorized to contribute to the any of the following project(s) associated with the CLA Group CLAGroupFoo: Project1, Project2") +} + +func TestRequestToAuthorizeTemplate(t *testing.T) { + params := RequestToAuthorizeTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + CorporateConsole: "https://CorporateConsoleURLValue", + }, + CLAManagers: []ClaManagerInfoParams{ + {LfUsername: "LFUserName", Email: "LFEmail"}, + }, + ContributorName: "ContributorNameValue", + ContributorEmail: "ContributorEmailValue", + CompanyID: "CompanyIDValue", + } + + result, err := RenderTemplate(utils.V1, RequestToAuthorizeTemplateName, RequestToAuthorizeTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProjectExternal and CLA Group JohnsProject") + assert.Contains(t, result, "ContributorNameValue (ContributorEmailValue) has requested") + assert.Contains(t, result, "") + assert.Contains(t, result, "contributing to JohnsProjectExternal on behalf of JohnsCompany") + + params.OptionalMessage = "OptionalMessageValue" + result, err = RenderTemplate(utils.V1, RequestToAuthorizeTemplateName, RequestToAuthorizeTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "ContributorNameValue included the following message") + assert.Contains(t, result, "

    OptionalMessageValue


    ") + +} diff --git a/cla-backend-go/emails/cla_manager_templates.go b/cla-backend-go/emails/cla_manager_templates.go new file mode 100644 index 000000000..4c7b85e45 --- /dev/null +++ b/cla-backend-go/emails/cla_manager_templates.go @@ -0,0 +1,304 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +// RemovedCLAManagerTemplateParams is email params for RemovedCLAManagerTemplate +type RemovedCLAManagerTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + CLAManagers []ClaManagerInfoParams +} + +const ( + // RemovedCLAManagerTemplateName is name of the RemovedCLAManagerTemplate + RemovedCLAManagerTemplateName = "RemovedCLAManagerTemplate" + // RemovedCLAManagerTemplate includes the email template for email when user is removed as CLA Manager + RemovedCLAManagerTemplate = ` +

    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}}:

    + +` +) + +// RenderRemovedCLAManagerTemplate renders the RemovedCLAManagerTemplate +func RenderRemovedCLAManagerTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params RemovedCLAManagerTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, RemovedCLAManagerTemplateName, RemovedCLAManagerTemplate, params) +} + +// RequestAccessToCLAManagersTemplateParams is email params for RequestAccessToCLAManagersTemplate +type RequestAccessToCLAManagersTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + RequesterName string + RequesterEmail string +} + +const ( + // RequestAccessToCLAManagersTemplateName is email template name for RequestAccessToCLAManagersTemplate + RequestAccessToCLAManagersTemplateName = "RequestAccessToCLAManagersTemplateName" + // RequestAccessToCLAManagersTemplate is email template for + RequestAccessToCLAManagersTemplate = ` +

    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}}.

    + +` +) + +// RenderRequestApprovedToCLAManagersTemplate renders the RemovedCLAManagerTemplate +func RenderRequestApprovedToCLAManagersTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params RequestApprovedToCLAManagersTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, RequestApprovedToCLAManagersTemplateName, RequestApprovedToCLAManagersTemplate, params) +} + +// RequestApprovedToRequesterTemplateParams email template params for RequestApprovedToRequesterTemplate +type RequestApprovedToRequesterTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams +} + +const ( + // RequestApprovedToRequesterTemplateName is email template name for RequestApprovedToRequesterTemplate + RequestApprovedToRequesterTemplateName = "RequestApprovedToRequesterTemplate" + // RequestApprovedToRequesterTemplate is email template for + RequestApprovedToRequesterTemplate = ` +

    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.

    + +` +) + +// RenderRequestDeniedToCLAManagersTemplate renders the RemovedCLAManagerTemplate +func RenderRequestDeniedToCLAManagersTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params RequestDeniedToCLAManagersTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, RequestDeniedToCLAManagersTemplateName, RequestDeniedToCLAManagersTemplate, params) +} + +// RequestDeniedToRequesterTemplateParams is email params for RequestDeniedToRequesterTemplate +type RequestDeniedToRequesterTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams +} + +const ( + // RequestDeniedToRequesterTemplateName is email template name for RequestDeniedToRequesterTemplate + RequestDeniedToRequesterTemplateName = "RequestDeniedToRequesterTemplate" + // RequestDeniedToRequesterTemplate is email template for + RequestDeniedToRequesterTemplate = ` +

    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}}.

    + +` +) + +// RenderClaManagerAddedToCLAManagersTemplate renders the ClaManagerAddedToCLAManagersTemplate +func RenderClaManagerAddedToCLAManagersTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params ClaManagerAddedToCLAManagersTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(claGroupModelVersion, ClaManagerAddedToCLAManagersTemplateName, ClaManagerAddedToCLAManagersTemplate, params) +} + +// ClaManagerDeletedToCLAManagersTemplateParams is template params for ClaManagerDeletedToCLAManagersTemplate +type ClaManagerDeletedToCLAManagersTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + Name string + Email string +} + +const ( + // ClaManagerDeletedToCLAManagersTemplateName is template name for ClaManagerDeletedToCLAManagersTemplate + ClaManagerDeletedToCLAManagersTemplateName = "ClaManagerDeletedToCLAManagersTemplate" + // ClaManagerDeletedToCLAManagersTemplate is template for + ClaManagerDeletedToCLAManagersTemplate = ` +

    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, "
  • LFUserName LFEmail
  • ") + + // even if the foundation is set we should show the project name + // because 0 child projects under the claGroup + params.CLAGroupTemplateParams.Projects[0].FoundationName = "CNCF" + result, err = RenderTemplate(utils.V1, RemovedCLAManagerTemplateName, RemovedCLAManagerTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "for the project JohnsProject") + + // then we increase the child project count so we should get the FoundationName instead of project name + params.ChildProjectCount = 2 + 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 CNCF") +} + +func TestRequestAccessToCLAManagersTemplate(t *testing.T) { + params := RequestAccessToCLAManagersTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{ + {ExternalProjectName: "JohnsProjectExternal"}, + }, + CLAGroupName: "JohnsProject", + CorporateConsole: "http://CorporateURL.com", + }, + RequesterName: "RequesterName", + RequesterEmail: "RequesterEmail", + } + + result, err := RenderTemplate(utils.V1, RequestAccessToCLAManagersTemplateName, RequestAccessToCLAManagersTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProject") + assert.Contains(t, result, "from JohnsCompany for the project JohnsProject") + assert.Contains(t, result, "contribute to JohnsProject") + assert.Contains(t, result, "CLA Managers for JohnsProject") + assert.Contains(t, result, "RequesterName (RequesterEmail) has requested") + assert.Contains(t, result, "another CLA Manager from JohnsCompany for JohnsProject") + assert.Contains(t, result, "") + assert.Contains(t, result, "then select the JohnsProjectExternal project") + +} + +func TestRequestApprovedToCLAManagersTemplate(t *testing.T) { + params := RequestApprovedToCLAManagersTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{ + {ExternalProjectName: "JohnsProjectExternal"}, + }, + CLAGroupName: "JohnsProject", + }, + RequesterName: "RequesterName", + RequesterEmail: "RequesterEmail", + } + + result, err := RenderTemplate(utils.V1, RequestApprovedToCLAManagersTemplateName, RequestApprovedToCLAManagersTemplate, + 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, "allowed to contribute to JohnsProject") + assert.Contains(t, result, "CLA Managers for JohnsProject") + assert.Contains(t, result, "
  • RequesterName (RequesterEmail)
  • ") +} + +func TestRequestApprovedToRequesterTemplateParams(t *testing.T) { + params := RequestApprovedToRequesterTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + CorporateConsole: "http://CorporateURL.com", + }, + } + + result, err := RenderTemplate(utils.V1, RequestApprovedToRequesterTemplateName, RequestApprovedToRequesterTemplate, + 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, "allowed to contribute to JohnsProject") + assert.Contains(t, result, "CLA Managers for JohnsProject") + assert.Contains(t, result, "
    ") + assert.Contains(t, result, "and then the project JohnsProject") +} + +func TestRequestDeniedToCLAManagersTemplate(t *testing.T) { + params := RequestDeniedToCLAManagersTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + }, + RequesterName: "RequesterName", + RequesterEmail: "RequesterEmail", + } + + result, err := RenderTemplate(utils.V1, RequestDeniedToCLAManagersTemplateName, RequestDeniedToCLAManagersTemplate, + 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, "allowed to contribute to JohnsProject") + assert.Contains(t, result, "
  • RequesterName (RequesterEmail)
  • ") +} + +func TestRequestDeniedToRequesterTemplate(t *testing.T) { + params := RequestDeniedToRequesterTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + }, + } + + result, err := RenderTemplate(utils.V1, RequestDeniedToRequesterTemplateName, RequestDeniedToRequesterTemplate, + 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, "allowed to contribute to JohnsProject") +} + +func TestClaManagerAddedEToUserTemplate(t *testing.T) { + params := ClaManagerAddedEToUserTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + CorporateConsole: "http://CorporateURL.com", + }, + } + + result, err := RenderTemplate(utils.V1, ClaManagerAddedEToUserTemplateName, ClaManagerAddedEToUserTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProjectExternal") + assert.Contains(t, result, "CLA Manager for the organization JohnsCompany and the project JohnsProjectExternal") + assert.Contains(t, result, "allowed to contribute to the project JohnsProjectExternal") + assert.Contains(t, result, "CLA Managers for the CLA Group JohnsProject") + assert.Contains(t, result, "
    ") + assert.Contains(t, result, "and then the project JohnsProject") +} + +func TestClaManagerAddedToCLAManagersTemplate(t *testing.T) { + params := ClaManagerAddedToCLAManagersTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + }, + Name: "John", + Email: "john@example.com", + } + + result, err := RenderTemplate(utils.V1, ClaManagerAddedToCLAManagersTemplateName, ClaManagerAddedToCLAManagersTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProjectExternal associated with the CLA Group JohnsProject") + assert.Contains(t, result, "CLA Manager from JohnsCompany for the project JohnsProjectExternal") + assert.Contains(t, result, "contribute to JohnsProjectExternal") + assert.Contains(t, result, "CLA Managers for JohnsProjectExternal") + assert.Contains(t, result, "
  • John (john@example.com)
  • ") + +} + +func TestClaManagerDeletedToCLAManagersTemplate(t *testing.T) { + params := ClaManagerDeletedToCLAManagersTemplateParams{ + CommonEmailParams: CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: CLAGroupTemplateParams{ + Projects: []CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal"}}, + CLAGroupName: "JohnsProject", + }, + Name: "John", + Email: "john@example.com", + } + + result, err := RenderTemplate(utils.V1, ClaManagerDeletedToCLAManagersTemplateName, ClaManagerDeletedToCLAManagersTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project JohnsProject") + assert.Contains(t, result, "John (john@example.com) has been removed") + +} diff --git a/cla-backend-go/emails/docusign_templates.go b/cla-backend-go/emails/docusign_templates.go new file mode 100644 index 000000000..b96781297 --- /dev/null +++ b/cla-backend-go/emails/docusign_templates.go @@ -0,0 +1,47 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package emails + +type DocumentSignedTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + ICLA bool + PdfLink string +} + +const ( + // DocumentSignedTemplateName is email template name for DocumentSignedTemplate + DocumentSignedTemplateName = "DocumentSignedTemplate" + + // DocumentSignedTemplate is email template for + DocumentSignedICLATemplate = ` +

    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:

    +
      +
    1. After login, you will be redirected to the portal {{.CorporateConsole}} where you can either sign the CLA for any of the project(s): {{range $index, $projectName := .Projects}}{{if $index}},{{end}}{{$projectName.GetProjectFullURL}}{{end}}, or send it to an authorized signatory for your company.
    2. +
    3. After signing the CLA, you will need to add this contributor to the approved list in the CLA Manager console.
    4. +
    5. After adding the contributor, please notify them so that they can complete the contribution process.
    6. +
    +` +) + +// RenderV2ToCLAManagerDesigneeTemplate renders V2ToCLAManagerDesigneeTemplate +func RenderV2ToCLAManagerDesigneeTemplate(svc EmailTemplateService, projectSFIDs []string, params V2ToCLAManagerDesigneeTemplateParams, template string, templateName string) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFIDs[0]) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + return RenderTemplate(utils.V2, templateName, + template, params) +} + +const ( + // V2DesigneeToUserWithNoLFIDTemplateName is email template name for V2DesigneeToUserWithNoLFIDTemplate + V2DesigneeToUserWithNoLFIDTemplateName = "V2DesigneeToUserWithNoLFIDTemplateName" + // V2DesigneeToUserWithNoLFIDTemplate is email template for + V2DesigneeToUserWithNoLFIDTemplate = ` +

    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:

    +
      +
    1. Please click on Accept Invite to create your LF Login.This is used to access the EasyCLA CLA Manager console.
    2. +
    3. After login, you will be redirected to the portal {{.CorporateConsole}} where you can either sign the CLA for any of the project(s): {{range $index, $projectName := .Projects}}{{if $index}},{{end}}{{$projectName.GetProjectFullURL}}{{end}}, or send it to an authorized signatory for your company.
    4. +
    5. After signing the CLA, you will need to add this contributor to the approved list in the CLA Manager console.
    6. +
    7. After adding the contributor, please notify them so that they can complete the contribution process.
    8. +
    +` +) + +// V2CLAManagerToUserWithNoLFIDTemplateParams is email params for V2CLAManagerToUserWithNoLFIDTemplate +type V2CLAManagerToUserWithNoLFIDTemplateParams struct { + CommonEmailParams + CLAGroupTemplateParams + RequesterUserName string + RequesterEmail string + Projects []CLAProjectParams +} + +const ( + // V2CLAManagerToUserWithNoLFIDTemplateName is email template name + V2CLAManagerToUserWithNoLFIDTemplateName = "V2CLAManagerToUserWithNoLFIDTemplate" + // V2CLAManagerToUserWithNoLFIDTemplate is email template + V2CLAManagerToUserWithNoLFIDTemplate = ` +

    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:

    +
      +
    1. Please click on Accept Invite to create your LF Login.This is used to access the EasyCLA CLA Manager console.
    2. +
    3. After login, you will be redirected to the portal {{.Project.CorporateConsole}} where you can either sign the CLA for the project: {{.Project.GetProjectFullURL}}, or send it to an authorized signatory for your company.
    4. +
    5. After signing the CLA, you will need to add this contributor to the approved list in the CLA Manager console.
    6. +
    7. After adding the contributor, please notify them so that they can complete the contribution process.
    8. +
    + +` +) + +// RenderV2CLAManagerToUserWithNoLFIDTemplate renders V2CLAManagerToUserWithNoLFIDTemplate +func RenderV2CLAManagerToUserWithNoLFIDTemplate(svc EmailTemplateService, projectSFID string, params V2CLAManagerToUserWithNoLFIDTemplateParams) (string, error) { + claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(utils.V2, projectSFID) + if err != nil { + return "", err + } + params.CLAGroupTemplateParams = claGroupParams + + body, err := RenderTemplate(utils.V2, V2CLAManagerToUserWithNoLFIDTemplateName, + V2CLAManagerToUserWithNoLFIDTemplate, + params) + return body, err +} diff --git a/cla-backend-go/events/event_data.go b/cla-backend-go/events/event_data.go index 097f70186..8b43c71be 100644 --- a/cla-backend-go/events/event_data.go +++ b/cla-backend-go/events/event_data.go @@ -1,10 +1,18 @@ // Copyright The Linux Foundation and each contributor to CommunityBridge. // SPDX-License-Identifier: MIT -//nolint + package events import ( "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/utils" +) + +const ( + // noReason constant value + noReason = "No reason" ) // EventData returns event data string which is used for event logging and containsPII field @@ -13,102 +21,165 @@ type EventData interface { GetEventSummaryString(args *LogEventArgs) (eventData string, containsPII bool) } -// RepositoryAddedEventData . . . +// CLAGroupEnrolledProjectData event data model +type CLAGroupEnrolledProjectData struct { +} + +// CLAGroupUnenrolledProjectData event data model +type CLAGroupUnenrolledProjectData struct { +} + +// ProjectServiceCLAEnabledData event data model +type ProjectServiceCLAEnabledData struct { +} + +// ProjectServiceCLADisabledData event data model +type ProjectServiceCLADisabledData struct { +} + +// RepositoryAddedEventData event data model type RepositoryAddedEventData struct { RepositoryName string + RepositoryType string } -// RepositoryDisabledEventData . . . +// RepositoryDisabledEventData event data model type RepositoryDisabledEventData struct { - RepositoryName string + RepositoryName string + RepositoryExternalID int64 + RepositoryType string +} + +// RepositoryDeletedEventData event data model +type RepositoryDeletedEventData struct { + RepositoryName string + RepositoryExternalID int64 +} + +// RepositoryRenamedEventData event data model +type RepositoryRenamedEventData struct { + NewRepositoryName string + OldRepositoryName string +} + +// RepositoryTransferredEventData event data model +type RepositoryTransferredEventData struct { + RepositoryName string + OldGithubOrgName string + NewGithubOrgName string } -// RepositoryUpdatedEventData . . . +// RepositoryUpdatedEventData event data model type RepositoryUpdatedEventData struct { RepositoryName string } -// RepositoryBranchProtectionAddedEventData . . . +// RepositoryBranchProtectionAddedEventData event data model type RepositoryBranchProtectionAddedEventData struct { RepositoryName string } -// RepositoryBranchProtectionDisabledEventData . . . +// RepositoryBranchProtectionDisabledEventData event data model type RepositoryBranchProtectionDisabledEventData struct { RepositoryName string } -// RepositoryBranchProtectionUpdatedEventData . . . +// RepositoryBranchProtectionUpdatedEventData event data model type RepositoryBranchProtectionUpdatedEventData struct { RepositoryName string } -// GerritProjectDeletedEventData . . . +// GerritProjectDeletedEventData event data model type GerritProjectDeletedEventData struct { DeletedCount int } -// GerritAddedEventData . . . +// GerritAddedEventData data model type GerritAddedEventData struct { GerritRepositoryName string } -// GerritDeletedEventData . . . +// GerritDeletedEventData data model type GerritDeletedEventData struct { GerritRepositoryName string } -// GitHubProjectDeletedEventData . . . +// GerritUserAddedEventData data model +type GerritUserAddedEventData struct { + Username string + GroupName string +} + +// GerritUserRemovedEventData data model +type GerritUserRemovedEventData struct { + Username string + GroupName string +} + +// GitHubProjectDeletedEventData data model type GitHubProjectDeletedEventData struct { DeletedCount int } -// SignatureProjectInvalidatedEventData . . . +// SignatureProjectInvalidatedEventData data model type SignatureProjectInvalidatedEventData struct { InvalidatedCount int } -// UserCreatedEventData . . . +// SignatureInvalidatedApprovalRejectionEventData data model +type SignatureInvalidatedApprovalRejectionEventData struct { + GHUsername string + Email string + SignatureID string + CLAManager *models.User + CLAGroupID string +} + +// UserCreatedEventData data model type UserCreatedEventData struct{} -// UserDeletedEventData . . . +// UserDeletedEventData data model type UserDeletedEventData struct { DeletedUserID string } -// UserUpdatedEventData . . . +// UserUpdatedEventData data model type UserUpdatedEventData struct{} -// CompanyACLRequestAddedEventData . . . +// CompanyACLRequestAddedEventData data model type CompanyACLRequestAddedEventData struct { UserName string UserID string UserEmail string } -// CompanyACLRequestApprovedEventData . . . +// CompanyACLRequestApprovedEventData data model type CompanyACLRequestApprovedEventData struct { UserName string UserID string UserEmail string } -// CompanyACLRequestDeniedEventData . . . +// CompanyACLRequestDeniedEventData data model type CompanyACLRequestDeniedEventData struct { UserName string UserID string UserEmail string } -// CompanyACLUserAddedEventData . . . +// CompanyACLUserAddedEventData data model type CompanyACLUserAddedEventData struct { UserLFID string } -// CLATemplateCreatedEventData . . . -type CLATemplateCreatedEventData struct{} +// CLATemplateCreatedEventData data model +type CLATemplateCreatedEventData struct { + TemplateName string + OldPOC string + NewPOC string +} -// GitHubOrganizationAddedEventData . . . +// GitHubOrganizationAddedEventData data model type GitHubOrganizationAddedEventData struct { GitHubOrganizationName string AutoEnabled bool @@ -116,34 +187,56 @@ type GitHubOrganizationAddedEventData struct { BranchProtectionEnabled bool } -// GitHubOrganizationDeletedEventData . . . +// GitHubOrganizationDeletedEventData data model type GitHubOrganizationDeletedEventData struct { GitHubOrganizationName string } -// GitHubOrganizationUpdatedEventData . . . +// GitHubOrganizationUpdatedEventData data model type GitHubOrganizationUpdatedEventData struct { - GitHubOrganizationName string + GitHubOrganizationName string + AutoEnabled bool + AutoEnabledClaGroupID string + BranchProtectionEnabled bool +} + +// GitLabOrganizationAddedEventData data model +type GitLabOrganizationAddedEventData struct { + GitLabOrganizationName string + AutoEnabled bool + AutoEnabledClaGroupID string + BranchProtectionEnabled bool +} + +// GitLabOrganizationDeletedEventData data model +type GitLabOrganizationDeletedEventData struct { + GitLabOrganizationName string +} + +// GitLabOrganizationUpdatedEventData data model +type GitLabOrganizationUpdatedEventData struct { + GitLabOrganizationName string + GitLabGroupID int64 AutoEnabled bool AutoEnabledClaGroupID string } -// CCLAApprovalListRequestCreatedEventData . . . +// CCLAApprovalListRequestCreatedEventData data model type CCLAApprovalListRequestCreatedEventData struct { RequestID string } -// CCLAApprovalListRequestApprovedEventData . . . +// CCLAApprovalListRequestApprovedEventData data model type CCLAApprovalListRequestApprovedEventData struct { RequestID string } -// CCLAApprovalListRequestRejectedEventData . . . +// CCLAApprovalListRequestRejectedEventData data model type CCLAApprovalListRequestRejectedEventData struct { RequestID string } -// CLAManagerCreatedEventData . . . +// CLAManagerCreatedEventData data model type CLAManagerCreatedEventData struct { CompanyName string ProjectName string @@ -152,7 +245,7 @@ type CLAManagerCreatedEventData struct { UserLFID string } -// CLAManagerDeletedEventData . . . +// CLAManagerDeletedEventData data model type CLAManagerDeletedEventData struct { CompanyName string ProjectName string @@ -161,7 +254,7 @@ type CLAManagerDeletedEventData struct { UserLFID string } -// CLAManagerRequestCreatedEventData . . . +// CLAManagerRequestCreatedEventData data model type CLAManagerRequestCreatedEventData struct { RequestID string CompanyName string @@ -171,7 +264,7 @@ type CLAManagerRequestCreatedEventData struct { UserLFID string } -// CLAManagerRequestApprovedEventData . . . +// CLAManagerRequestApprovedEventData data model type CLAManagerRequestApprovedEventData struct { RequestID string CompanyName string @@ -182,7 +275,7 @@ type CLAManagerRequestApprovedEventData struct { ManagerEmail string } -// CLAManagerRequestDeniedEventData . . . +// CLAManagerRequestDeniedEventData data model type CLAManagerRequestDeniedEventData struct { RequestID string CompanyName string @@ -193,7 +286,7 @@ type CLAManagerRequestDeniedEventData struct { ManagerEmail string } -// CLAManagerRequestDeletedEventData . . . +// CLAManagerRequestDeletedEventData data model type CLAManagerRequestDeletedEventData struct { RequestID string CompanyName string @@ -204,131 +297,134 @@ type CLAManagerRequestDeletedEventData struct { ManagerEmail string } -// CLAApprovalListAddEmailData . . . +// CLAApprovalListAddEmailData data model type CLAApprovalListAddEmailData struct { - UserName string - UserEmail string - UserLFID string ApprovalListEmail string } -// CLAApprovalListRemoveEmailData . . . +// CLAApprovalListRemoveEmailData data model type CLAApprovalListRemoveEmailData struct { - UserName string - UserEmail string - UserLFID string ApprovalListEmail string } -// CLAApprovalListAddDomainData . . . +// CLAApprovalListAddDomainData data model type CLAApprovalListAddDomainData struct { - UserName string - UserEmail string - UserLFID string ApprovalListDomain string } -// CLAApprovalListRemoveDomainData . . . +// CLAApprovalListRemoveDomainData data model type CLAApprovalListRemoveDomainData struct { - UserName string - UserEmail string - UserLFID string ApprovalListDomain string } -// CLAApprovalListAddGitHubUsernameData . . . +// CLAApprovalListAddGitHubUsernameData data model type CLAApprovalListAddGitHubUsernameData struct { - UserName string - UserEmail string - UserLFID string ApprovalListGitHubUsername string } -// CLAApprovalListRemoveGitHubUsernameData . . . +// CLAApprovalListRemoveGitHubUsernameData data model type CLAApprovalListRemoveGitHubUsernameData struct { - UserName string - UserEmail string - UserLFID string ApprovalListGitHubUsername string } -// CLAApprovalListAddGitHubOrgData . . . +// CLAApprovalListAddGitHubOrgData data model type CLAApprovalListAddGitHubOrgData struct { - UserName string - UserEmail string - UserLFID string ApprovalListGitHubOrg string } -// CLAApprovalListRemoveGitHubOrgData . . . +// CLAApprovalListRemoveGitHubOrgData data model type CLAApprovalListRemoveGitHubOrgData struct { - UserName string - UserEmail string - UserLFID string ApprovalListGitHubOrg string } -// ApprovalListGitHubOrganizationAddedEventData . . . +// CLAApprovalListAddGitLabUsernameData data model +type CLAApprovalListAddGitLabUsernameData struct { + ApprovalListGitLabUsername string +} + +// CLAApprovalListRemoveGitLabUsernameData data model +type CLAApprovalListRemoveGitLabUsernameData struct { + ApprovalListGitLabUsername string +} + +// CLAApprovalListAddGitLabGroupData data model +type CLAApprovalListAddGitLabGroupData struct { + ApprovalListGitLabGroup string +} + +// CLAApprovalListRemoveGitLabGroupData data model +type CLAApprovalListRemoveGitLabGroupData struct { + ApprovalListGitLabGroup string +} + +// ApprovalListGitHubOrganizationAddedEventData data model type ApprovalListGitHubOrganizationAddedEventData struct { GitHubOrganizationName string } -// ApprovalListGitHubOrganizationDeletedEventData . . . +// ApprovalListGitHubOrganizationDeletedEventData data model type ApprovalListGitHubOrganizationDeletedEventData struct { GitHubOrganizationName string } -// ClaManagerAccessRequestAddedEventData . . . +// ClaManagerAccessRequestAddedEventData data model type ClaManagerAccessRequestAddedEventData struct { ProjectName string CompanyName string } -// ClaManagerAccessRequestDeletedEventData . . . +// ClaManagerAccessRequestDeletedEventData data model type ClaManagerAccessRequestDeletedEventData struct { RequestID string } -// CLAGroupCreatedEventData . . . +// CLAGroupCreatedEventData data model type CLAGroupCreatedEventData struct{} -// CLAGroupUpdatedEventData . . . +// CLAGroupUpdatedEventData data model type CLAGroupUpdatedEventData struct { - ClaGroupName string - ClaGroupDescription string + NewClaGroupName string + NewClaGroupDescription string + OldClaGroupName string + OldClaGroupDescription string } -// CLAGroupDeletedEventData . . . +// CLAGroupDeletedEventData data model type CLAGroupDeletedEventData struct{} -// ContributorNotifyCompanyAdminData . . . +// ContributorNotifyCompanyAdminData data model type ContributorNotifyCompanyAdminData struct { AdminName string AdminEmail string } -// ContributorNotifyCLADesignee . . . +// ContributorNotifyCLADesignee data model type ContributorNotifyCLADesignee struct { DesigneeName string DesigneeEmail string } -// ContributorAssignCLADesignee . . . +// ContributorAssignCLADesignee data model type ContributorAssignCLADesignee struct { DesigneeName string DesigneeEmail string } -// UserConvertToContactData . . . -type UserConvertToContactData struct{} +// UserConvertToContactData data model +type UserConvertToContactData struct { + UserName string + UserEmail string +} -// AssignRoleScopeData . . . +// AssignRoleScopeData data model type AssignRoleScopeData struct { - Role string - Scope string + Role string + Scope string + UserName string + UserEmail string } -// ClaManagerRoleCreatedData . . . +// ClaManagerRoleCreatedData data model type ClaManagerRoleCreatedData struct { Role string Scope string @@ -336,7 +432,7 @@ type ClaManagerRoleCreatedData struct { UserEmail string } -// ClaManagerRoleDeletedData . . . +// ClaManagerRoleDeletedData data model type ClaManagerRoleDeletedData struct { Role string Scope string @@ -344,737 +440,2332 @@ type ClaManagerRoleDeletedData struct { UserEmail string } -// GetEventDetailsString . . . +// SignatureAutoCreateECLAUpdatedEventData data model +type SignatureAutoCreateECLAUpdatedEventData struct { + AutoCreateECLA bool +} + +type IndividualSignatureSignedEventData struct { + ProjectName string + Username string + ProjectID string +} + +type CorporateSignatureSignedEventData struct { + ProjectName string + CompanyName string + SignatoryName string +} + +func (ed *CorporateSignatureSignedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The signature was signed for the project %s and company %s by %s", args.ProjectName, ed.CompanyName, ed.SignatoryName) + if args.UserName != "" { + data = fmt.Sprintf("%s by the user %s", data, args.UserName) + } + data = fmt.Sprintf("%s.", data) + return data, true +} + +func (ed *CorporateSignatureSignedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The signature was signed for the project %s and company %s by %s", args.ProjectName, ed.CompanyName, ed.SignatoryName) + data = fmt.Sprintf("%s.", data) + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *SignatureAutoCreateECLAUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + + data := "Auto-create ECLAs for contributors was" + if ed.AutoCreateECLA { + data = data + " enabled" + } else { + data = data + " disabled" + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *CLAGroupEnrolledProjectData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The project %s (%s) was enrolled into the CLA Group %s (%s)", args.ProjectName, args.ProjectID, args.CLAGroupName, args.CLAGroupID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *CLAGroupUnenrolledProjectData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The project %s (%s) was unenrolled from the CLA Group %s (%s)", args.ProjectName, args.ProjectID, args.CLAGroupName, args.CLAGroupID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *ProjectServiceCLAEnabledData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The CLA Service for the project %s (%s) was enabled", args.ProjectName, args.ProjectID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *ProjectServiceCLADisabledData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The CLA Service for the project %s (%s) was disabled", args.ProjectName, args.ProjectID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event func (ed *RepositoryAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository: %s was added for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + repositoryType := utils.GitHubRepositoryType + if ed.RepositoryType != "" { + repositoryType = ed.RepositoryType + } + data := fmt.Sprintf("The %s repository: %s was added for the project %s", repositoryType, ed.RepositoryName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *RepositoryDisabledEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository %s was deleted for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + repositoryType := utils.GitHubRepositoryType + if repositoryType != "" { + repositoryType = ed.RepositoryType + } + data := fmt.Sprintf("The %s repository", repositoryType) // nolint + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.RepositoryName != "" { + data = data + fmt.Sprintf(" with repository name %s", ed.RepositoryName) + } + if ed.RepositoryExternalID > 0 { + data = data + fmt.Sprintf(" with repository external ID %d", ed.RepositoryExternalID) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectSFID) + } + data = data + " was disabled" + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *RepositoryDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := "The GitHub repository " // nolint + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.RepositoryName != "" { + data = data + fmt.Sprintf(" with repository name %s", ed.RepositoryName) + } + if ed.RepositoryExternalID > 0 { + data = data + fmt.Sprintf(" with repository external ID %d", ed.RepositoryExternalID) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectSFID) + } + data = data + " was deleted" + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *RepositoryRenamedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitHub repository renamed from %s to %s for the project %s", ed.OldRepositoryName, ed.NewRepositoryName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *RepositoryTransferredEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitHub repository : %s transferred from %s to %s Github Organization for the project %s", ed.RepositoryName, ed.OldGithubOrgName, ed.NewGithubOrgName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *RepositoryUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository %s was updated for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository %s was updated for the project %s", ed.RepositoryName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *RepositoryBranchProtectionAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository branch protection %s was added for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository branch protection %s was added for the project %s", ed.RepositoryName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *RepositoryBranchProtectionDisabledEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository branch protection %s was disabled for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository branch protection %s was disabled for the project %s", ed.RepositoryName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *RepositoryBranchProtectionUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository branch protection %s was updated for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository branch protection %s was updated for the project %s", ed.RepositoryName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *UserCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s added. User Details: %+v.", args.userName, args.UserModel) + data := fmt.Sprintf("User was added : %+v", args.UserModel) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *UserUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - return fmt.Sprintf("User: %s updated. User Details: %+v.", args.userName, *args.UserModel), true + data := fmt.Sprintf("User details updated: %+v", *args.UserModel) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *UserDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s deleted. User ID: %s.", args.userName, ed.DeletedUserID) + data := fmt.Sprintf("User ID: %s was deleted", ed.DeletedUserID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CompanyACLRequestAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s added pending invite with ID: %s and Email: %s for Company: %s.", - ed.UserName, ed.UserID, ed.UserEmail, args.companyName) + data := fmt.Sprintf("User: %s added pending invite with ID: %s and Email: %s for Company: %s", + ed.UserName, ed.UserID, ed.UserEmail, args.CompanyName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CompanyACLRequestApprovedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("Access Aproved for User: %s, ID: %s, Email: %s Company Group: %s.", - ed.UserName, args.companyName, ed.UserID, ed.UserEmail) + data := fmt.Sprintf("Access Aproved for User: %s, ID: %s, Email: %s Company Group: %s", + ed.UserName, args.CompanyName, ed.UserID, ed.UserEmail) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CompanyACLRequestDeniedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("Access Denied for User: %s, ID: %s, Email: %s Company Group: %s.", - ed.UserName, args.companyName, ed.UserID, ed.UserEmail) + ed.UserName, args.CompanyName, ed.UserID, ed.UserEmail) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CompanyACLUserAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User with LF Username: %s added to the ACL for Company: %s by: %s.", - ed.UserLFID, args.companyName, args.userName) + data := fmt.Sprintf("User with LF Username: %s added to the ACL for Company: %s", + args.LFUser.Name, args.CompanyName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLATemplateCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("PDF Templates created for Project: %s by: %s.", args.userName, args.projectName) + data := "A CLA Group template was created or updated" // nolint + if args.CLAGroupName != "" { + data = fmt.Sprintf("%s for the CLA Group %s", data, args.CLAGroupName) + } + if ed.TemplateName != "" { + data = fmt.Sprintf("%s using template: %s", data, ed.TemplateName) + } + if args.ProjectName != "" { + data = fmt.Sprintf("%s for the project %s", data, args.ProjectName) + } + if args.UserName != "" { + data = fmt.Sprintf("%s by the user %s", data, args.UserName) + } + data = fmt.Sprintf("%s.", data) + + if ed.OldPOC != "" && ed.NewPOC != "" { + data = fmt.Sprintf("%s The point of contact email was changed from %s to %s.", data, ed.OldPOC, ed.NewPOC) + } else if ed.NewPOC != "" { + data = fmt.Sprintf("%s The point of contact email was set to %s.", data, ed.NewPOC) + } + return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *GitHubOrganizationAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("GitHub Organization: %s was added with auto-enabled: %t, with branch protection enabled: %t", ed.GitHubOrganizationName, ed.AutoEnabled, ed.BranchProtectionEnabled) if ed.AutoEnabledClaGroupID != "" { data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) } - data = data + fmt.Sprintf(" by: %s.", args.userName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *GitHubOrganizationDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Organization: %s was deleted by: %s.", - ed.GitHubOrganizationName, args.userName) + data := fmt.Sprintf("GitHub Organization: %s was deleted ", ed.GitHubOrganizationName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *GitHubOrganizationUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Organization:%s was updated with auto-enabled: %t", - ed.GitHubOrganizationName, ed.AutoEnabled) + data := fmt.Sprintf("The GitHub Organization '%s' was updated", ed.GitHubOrganizationName) + data = data + fmt.Sprintf(" with auto-enabled set to %t", ed.AutoEnabled) + data = data + fmt.Sprintf(" with branch protection set to %t", ed.BranchProtectionEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = data + fmt.Sprintf(" with auto-enabled-cla-group ID value of %s", ed.AutoEnabledClaGroupID) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *GitLabOrganizationAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("GitLab Group: %s was added with auto-enabled: %t, with branch protection enabled: %t", + ed.GitLabOrganizationName, ed.AutoEnabled, ed.BranchProtectionEnabled) if ed.AutoEnabledClaGroupID != "" { data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) } - data = data + fmt.Sprintf("by: %s.", args.userName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *GitLabOrganizationDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("GitLab Group: %s was deleted ", ed.GitLabOrganizationName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *GitLabOrganizationUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := "GitLab Group" // nolint + if ed.GitLabOrganizationName != "" { + data = fmt.Sprintf("%s with name: %s", data, ed.GitLabOrganizationName) + } + if ed.GitLabGroupID > 0 { + data = fmt.Sprintf("%s with group ID: %d", data, ed.GitLabGroupID) + } + data = fmt.Sprintf("%s was updated with auto-enabled: %t", data, ed.AutoEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = fmt.Sprintf("%s with auto-enabled-cla-group: %s", data, ed.AutoEnabledClaGroupID) + } + if args.ProjectName != "" { + data = fmt.Sprintf("%s for the project %s", data, args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.UserName != "" { + data = fmt.Sprintf("%s by the user %s", data, args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CCLAApprovalListRequestApprovedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s approved a CCLA Approval Request for Project: %s and Company: %s with Request ID: %s.", - args.userName, args.projectName, args.companyName, ed.RequestID) + args.UserName, args.ProjectName, args.CompanyName, ed.RequestID) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CCLAApprovalListRequestRejectedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s rejected a CCLA Approval Request for Project: %s, Company: %s with Request ID: %s.", - args.userName, args.projectName, args.companyName, ed.RequestID) + args.UserName, args.ProjectName, args.CompanyName, ed.RequestID) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAManagerRequestCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s, LFID: %s, Email: %s added CLA Manager Request: %s for Company: %s, Project: %s.", ed.UserName, ed.UserLFID, ed.UserEmail, ed.RequestID, ed.CompanyName, ed.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAManagerCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s, LFID: %s, Email: %s was added as CLA Manager for Company: %s, Project: %s.", - ed.UserName, ed.UserLFID, ed.UserEmail, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The user: %s LFID: %s, email: %s was added as CLA Manager", ed.UserName, ed.UserLFID, ed.UserEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", ed.ProjectName) + } else { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectSFID) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAManagerDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s LFID: %s, Email: %s was removed as CLA Manager for Company: %s, Project: %s.", - ed.UserName, ed.UserLFID, ed.UserEmail, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The user: %s LFID: %s, email: %s was removed as CLA Manager", ed.UserName, ed.UserLFID, ed.UserEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", ed.ProjectName) + } else { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAManagerRequestApprovedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager Request: %s was approved for User %s, Email: %s by Manager: %s, Email: %s for Company: %s, Project: %s.", + data := fmt.Sprintf("CLA Manager Request: %s was approved for User %s, Email: %s by Manager: %s, Email: %s for Company: %s, Project: %s", ed.RequestID, ed.UserName, ed.UserEmail, ed.ManagerName, ed.ManagerEmail, ed.CompanyName, ed.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAManagerRequestDeniedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager Request: %s was denied for User %s, Email: %s by Manager: %s, Email: %s for Company: %s, Project: %s.", + data := fmt.Sprintf("CLA Manager Request: %s was denied for User %s, Email: %s by Manager: %s, Email: %s for Company: %s, Project: %s", ed.RequestID, ed.UserName, ed.UserEmail, ed.ManagerName, ed.ManagerEmail, ed.CompanyName, ed.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAManagerRequestDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager Request: %s was deleted for User %s, Email: %s by Manager: %s, Email: %s for Company: %s, Project: %s.", + data := fmt.Sprintf("CLA Manager Request: %s was deleted for User %s, Email: %s by Manager: %s, Email: %s for Company: %s, Project: %s", ed.RequestID, ed.UserName, ed.UserEmail, ed.ManagerName, ed.ManagerEmail, ed.CompanyName, ed.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListAddEmailData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s added Email: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListEmail, args.companyName, args.projectName) + data := fmt.Sprintf("The email address %s was added to the approval list", ed.ApprovalListEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListRemoveEmailData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s removed Email: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListEmail, args.companyName, args.projectName) + data := fmt.Sprintf("The email address %s was removed from the approval list", ed.ApprovalListEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListAddDomainData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s added Domain: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListDomain, args.companyName, args.projectName) + data := fmt.Sprintf("The email address domain %s was added to the approval list", ed.ApprovalListDomain) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListRemoveDomainData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s removed Domain %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListDomain, args.companyName, args.projectName) + data := fmt.Sprintf("The email address domain %s was removed from the approval list", ed.ApprovalListDomain) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListAddGitHubUsernameData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s added GitHub Username: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListGitHubUsername, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub username %s was added to the approval list", ed.ApprovalListGitHubUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListRemoveGitHubUsernameData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s removed GitHub Username: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListGitHubUsername, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub username %s was removed from the approval list", ed.ApprovalListGitHubUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListAddGitHubOrgData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s added GitHub Organization: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListGitHubOrg, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub organization %s was added to the approval list", ed.ApprovalListGitHubOrg) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAApprovalListRemoveGitHubOrgData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s, Email: %s, LFID: %s removed GitHub Organization: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.UserEmail, ed.UserLFID, ed.ApprovalListGitHubOrg, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub organization %s was removed from the approval list", ed.ApprovalListGitHubOrg) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *CLAApprovalListAddGitLabUsernameData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab username %s was added to the approval list", ed.ApprovalListGitLabUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event +func (ed *CLAApprovalListRemoveGitLabUsernameData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab username %s was removed from the approval list", ed.ApprovalListGitLabUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *CLAApprovalListAddGitLabGroupData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab group %s was added to the approval list", ed.ApprovalListGitLabGroup) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *CLAApprovalListRemoveGitLabGroupData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab group %s was removed from the approval list", ed.ApprovalListGitLabGroup) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event func (ed *CCLAApprovalListRequestCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s created a CCLA Approval Request for Project: %s, Company: %s with Request ID: %s.", - args.userName, args.projectName, args.companyName, ed.RequestID) + data := fmt.Sprintf("The CCLA Approval Request was created for the Project: %s, Company: %s with Request ID: %s", + args.ProjectName, args.CompanyName, ed.RequestID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ApprovalListGitHubOrganizationAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s added GitHub Organization: %s to the whitelist for Company %s, Project: %s.", - args.userName, ed.GitHubOrganizationName, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub Organization: %s was added to the approval list for the Company %s, Project: %s", + ed.GitHubOrganizationName, args.CompanyName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ApprovalListGitHubOrganizationDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s removed GitHub Organization: %s from the whitelist for Company: %s, Project: %s.", - args.userName, ed.GitHubOrganizationName, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub Organization: %s was removed from the approval list for the Company: %s, Project: %s", + ed.GitHubOrganizationName, args.CompanyName, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ClaManagerAccessRequestAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s has requested to be CLA Manager for Company %s, Project: %s.", - args.userName, ed.CompanyName, ed.ProjectName) + args.UserName, ed.CompanyName, ed.ProjectName) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ClaManagerAccessRequestDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s has deleted CLA Manager Request with ID: %s.", - args.userName, ed.RequestID) + args.UserName, ed.RequestID) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAGroupCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Group ID: %s, Name: %s was created by: %s.", - args.ProjectID, args.projectName, args.userName) + data := fmt.Sprintf("The CLA group %s was created", args.CLAGroupName) + if args.CLAGroupID != "" { + data = data + fmt.Sprintf(" with the CLA group ID %s", args.CLAGroupID) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *CLAGroupUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Group ID: %s was updated by: %s with Name: %s, Description: %s.", - args.ProjectID, args.userName, ed.ClaGroupName, ed.ClaGroupDescription) - return data, true -} + var nameUpdated, descriptionUpdated bool -// GetEventDetailsString . . . + data := "The CLA Group" // nolint + if ed.NewClaGroupName != "" && ed.OldClaGroupName != ed.NewClaGroupName { + data = fmt.Sprintf("%s name was updated to '%s'", data, ed.NewClaGroupName) + nameUpdated = true + } + + if args.CLAGroupID != "" { + data = data + fmt.Sprintf(" with the CLA group ID %s", args.CLAGroupID) + } + + if ed.NewClaGroupDescription != "" && ed.OldClaGroupDescription != ed.NewClaGroupDescription { + descriptionUpdated = true + if nameUpdated { + data = fmt.Sprintf("%s and the description was updated to '%s'", data, ed.NewClaGroupDescription) + } else { + data = fmt.Sprintf("%s description was updated to '%s'", data, ed.NewClaGroupDescription) + } + } + + //shouldn't happen + if !nameUpdated && !descriptionUpdated { + data = data + " was updated" + } + + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + return data + ".", true +} + +// GetEventDetailsString returns the details string for this event func (ed *CLAGroupDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Group ID: %s was deleted by: %s.", - args.ProjectID, args.userName) + data := fmt.Sprintf("The CLA group %s was deleted", args.CLAGroupName) + if args.CLAGroupID != "" { + data = data + fmt.Sprintf(" with the CLA group ID %s", args.CLAGroupID) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *GerritProjectDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("%d Gerrit Repositories were deleted due to CLA Group/Project: %s deletion.", - ed.DeletedCount, args.projectName) - return data, false + data := fmt.Sprintf("%d Gerrit Repositories were deleted due to CLA Group/Project: %s deletion", + ed.DeletedCount, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *GerritAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("Gerrit Repository: %s was added by: %s.", ed.GerritRepositoryName, args.userName) + data := fmt.Sprintf("Gerrit Repository: %s was added", ed.GerritRepositoryName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *GerritDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("Gerrit Repository: %s was deleted by: %s.", ed.GerritRepositoryName, args.userName) + data := fmt.Sprintf("Gerrit Repository: %s was deleted", ed.GerritRepositoryName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event +func (ed *GerritUserAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The username %s was add to the gerrit group %s", ed.Username, ed.GroupName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *GerritUserRemovedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The username %s was removed from the gerrit group %s", ed.Username, ed.GroupName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event func (ed *GitHubProjectDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("%d GitHub Repositories were deleted due to CLA Group/Project: [%s] deletion.", - ed.DeletedCount, args.projectName) + data := fmt.Sprintf("%d GitHub Repositories were deleted due to CLA Group/Project: [%s] deletion", + ed.DeletedCount, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *SignatureProjectInvalidatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("%d Signatures were invalidated (approved set to false) due to CLA Group/Project: %s deletion.", - ed.InvalidatedCount, args.projectName) + data := fmt.Sprintf("%d Signatures were invalidated (approved set to false) due to CLA Group/Project: %s deletion", + ed.InvalidatedCount, args.ProjectName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *SignatureInvalidatedApprovalRejectionEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + reason := noReason + if ed.Email != "" { + reason = fmt.Sprintf("Email: %s approval removal ", ed.Email) + } else if ed.GHUsername != "" { + reason = fmt.Sprintf("GH Username: %s approval removal ", ed.GHUsername) + } + data := fmt.Sprintf("Signature invalidated by %s (approved set to false) due to %s ", utils.GetBestUsername(ed.CLAManager), reason) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ContributorNotifyCompanyAdminData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s notified Company Admin: %s by Email: %s for Company ID: %s, Name: %s.", - args.userName, ed.AdminName, ed.AdminEmail, args.companyName, args.CompanyID) + args.UserName, ed.AdminName, ed.AdminEmail, args.CompanyName, args.CompanyID) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ContributorNotifyCLADesignee) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s notified CLA Designee: %s by Email: %s for Project Name : %s, ID: %s and Company Name: %s, ID: %s.", - args.userName, ed.DesigneeName, ed.DesigneeEmail, - args.projectName, args.ExternalProjectID, - args.companyName, args.CompanyID) + args.UserName, ed.DesigneeName, ed.DesigneeEmail, + args.ProjectName, args.ProjectSFID, + args.CompanyName, args.CompanyID) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ContributorAssignCLADesignee) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User Name: %s, Email: %s was assigned as CLA Manager Designee for project Name: %s, ID: %s and Company Name: %s, ID: %s by: %s.", + data := fmt.Sprintf("User Name: %s, Email: %s was assigned as CLA Manager Designee for project Name: %s, ID: %s and Company Name: %s, ID: %s", ed.DesigneeName, ed.DesigneeEmail, - args.projectName, args.ExternalProjectID, - args.companyName, args.CompanyID, args.userName) + args.ProjectName, args.ProjectSFID, + args.CompanyName, args.CompanyID) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *UserConvertToContactData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s was converted to Contact state for Project: %s.", - args.LfUsername, args.ExternalProjectID) + data := fmt.Sprintf("User: %s was converted to Contact state for Project: %s with ID: %s.", + args.UserName, args.ProjectName, args.ProjectSFID) return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *AssignRoleScopeData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s was assigned Scope: %s with Role: %s for Project: %s.", - args.LfUsername, - ed.Scope, ed.Role, args.ExternalProjectID) + data := fmt.Sprintf("The user '%s' with email '%s' was added to the role %s", ed.UserName, ed.UserEmail, ed.Role) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" with project SFID %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ClaManagerRoleCreatedData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s, Email: %s was added to Role: %s with Scope: %s by: %s.", ed.UserName, ed.UserEmail, ed.Role, ed.Scope, args.userName) + data := fmt.Sprintf("User: %s, Email: %s was added to Role: %s with Scope: %s by: %s.", ed.UserName, ed.UserEmail, ed.Role, ed.Scope, args.UserName) return data, false } -// GetEventDetailsString . . . +// GetEventDetailsString returns the details string for this event func (ed *ClaManagerRoleDeletedData) GetEventDetailsString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s, Email: %s was removed from Role: %s with Scope: %s by: %s.", ed.UserName, ed.UserEmail, ed.Role, ed.Scope, args.userName) + data := fmt.Sprintf("User: %s, Email: %s was removed from Role: %s with Scope: %s by: %s.", ed.UserName, ed.UserEmail, ed.Role, ed.Scope, args.UserName) return data, false } // Event Summary started -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event +func (ed *CLAGroupEnrolledProjectData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The project %s was enrolled into the CLA Group %s", args.ProjectName, args.CLAGroupName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *CLAGroupUnenrolledProjectData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The project %s was unenrolled from the CLA Group %s", args.ProjectName, args.CLAGroupName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *ProjectServiceCLAEnabledData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := "CLA Service was enabled" + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *ProjectServiceCLADisabledData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := "CLA Service was disabled" + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event func (ed *RepositoryAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Repository: %s was added to Project: %s by: %s.", ed.RepositoryName, args.projectName, args.userName) + repositoryType := utils.GitHubRepositoryType + if ed.RepositoryType != "" { + repositoryType = ed.RepositoryType + } + data := fmt.Sprintf("The %s repository %s was added", repositoryType, ed.RepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *RepositoryDisabledEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Repository: %s was deleted from Project: %s by: %s.", ed.RepositoryName, args.projectName, args.userName) + repositoryType := utils.GitHubRepositoryType + if ed.RepositoryType != "" { + repositoryType = ed.RepositoryType + } + data := fmt.Sprintf("The %s repository", repositoryType) // nolint + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.RepositoryName != "" { + data = data + fmt.Sprintf(" with repository name %s", ed.RepositoryName) + } + if ed.RepositoryExternalID > 0 { + data = data + fmt.Sprintf(" with repository external ID %d", ed.RepositoryExternalID) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectSFID) + } + data = data + " was disabled" + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *RepositoryDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := "The GitHub repository " // nolint + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.RepositoryName != "" { + data = data + fmt.Sprintf(" with repository name %s", ed.RepositoryName) + } + if ed.RepositoryExternalID > 0 { + data = data + fmt.Sprintf(" with repository external ID %d", ed.RepositoryExternalID) + } + if args.ProjectSFID != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectSFID) + } + data = data + " was deleted" + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event +func (ed *RepositoryRenamedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitHub repository was renamed from %s to %s", ed.OldRepositoryName, ed.NewRepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *RepositoryTransferredEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitHub repository : %s was transferred from %s to %s Github Organization", ed.RepositoryName, ed.OldGithubOrgName, ed.NewGithubOrgName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event func (ed *RepositoryUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Repository: %s was updated for the project project: %s by: %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository %s was updated", ed.RepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventSummaryString returns the details string for this event func (ed *RepositoryBranchProtectionAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository branch protection %s was added for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository branch protection %s was added", ed.RepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventSummaryString returns the details string for this event func (ed *RepositoryBranchProtectionDisabledEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository branch protection %s was disabled for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository branch protection %s was disabled", ed.RepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventDetailsString . . . +// GetEventSummaryString returns the details string for this event func (ed *RepositoryBranchProtectionUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The GitHub repository branch protection %s was updated for the project %s by the user %s.", ed.RepositoryName, args.projectName, args.userName) + data := fmt.Sprintf("The GitHub repository branch protection %s was updated", ed.RepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *UserCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s was added, User Details: %+v.", args.userName, args.UserModel) + data := fmt.Sprintf("The user %s was added with the user details: %+v.", args.UserName, args.UserModel) return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *UserUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - return fmt.Sprintf("User: %s was updated, User Details: %+v.", args.userName, *args.UserModel), true + return fmt.Sprintf("The user %s was updated with the user details: %+v.", args.UserName, *args.UserModel), true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *UserDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User ID : %s was deleted by: %s.", ed.DeletedUserID, args.userName) + data := fmt.Sprintf("The user ID %s was deleted by the user %s.", ed.DeletedUserID, args.UserName) return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CompanyACLRequestAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s with ID: %s, Email: %s requested Company Invite for Company: %s.", - ed.UserName, ed.UserID, ed.UserEmail, args.companyName) + data := fmt.Sprintf("The user %s with ID %s and with the email %s requested a company invitation", + ed.UserName, ed.UserID, ed.UserEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CompanyACLRequestApprovedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("Company Invite was approved access for User: %s with ID: %s, Email: %s for company: %s.", - ed.UserName, ed.UserID, ed.UserEmail, args.companyName) + data := fmt.Sprintf("A company invite was approved for the user %s with the ID of %s with the email %s", + ed.UserName, ed.UserID, ed.UserEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CompanyACLRequestDeniedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("Company Invite was denied access for User: %s with ID: %s, Email %s for Company: %s.", - ed.UserName, ed.UserID, ed.UserEmail, args.companyName) + data := fmt.Sprintf("A company invite was denied for the user %s with the ID of %s with the email %s", + ed.UserName, ed.UserID, ed.UserEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CompanyACLUserAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User with LF Username %s was added to the ACL for Company: %s by: %s.", - ed.UserLFID, args.companyName, args.userName) + data := fmt.Sprintf("The user with LF username %s was added to the access list for the company %s by the user %s.", + args.LFUser.Name, args.CompanyName, args.UserName) return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLATemplateCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("PDF templates were created for Project %s by: %s.", args.projectName, args.userName) - return data, true + // Same output as the details + return ed.GetEventDetailsString(args) } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GitHubOrganizationAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Organization: %s was added with auto-enabled: %t, branch protection enabled: %t", + data := fmt.Sprintf("The GitHub organization %s was added with auto-enabled set to %t with branch protection enabled set to %t", ed.GitHubOrganizationName, ed.AutoEnabled, ed.BranchProtectionEnabled) if ed.AutoEnabledClaGroupID != "" { - data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) + data = data + fmt.Sprintf(" with auto-enabled-cla-group set to %s", ed.AutoEnabledClaGroupID) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) } - data = data + fmt.Sprintf(" by: %s.", args.userName) + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GitHubOrganizationDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Organization: %s was deleted by: %s.", - ed.GitHubOrganizationName, args.userName) + data := fmt.Sprintf("The GitHub organization %s was deleted", ed.GitHubOrganizationName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GitHubOrganizationUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("GitHub Organization: %s was updated with auto-enabled: %t", - ed.GitHubOrganizationName, ed.AutoEnabled) + data := fmt.Sprintf("The GitHub Organization '%s' was updated", ed.GitHubOrganizationName) + data = data + fmt.Sprintf(" with auto-enabled set to %t", ed.AutoEnabled) + data = data + fmt.Sprintf(" with branch protection set to %t", ed.BranchProtectionEnabled) if ed.AutoEnabledClaGroupID != "" { - data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) + data = data + fmt.Sprintf(" with auto-enabled-cla-group ID value of %s", ed.AutoEnabledClaGroupID) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GitLabOrganizationAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab group %s was added with auto-enabled set to %t with branch protection enabled set to %t", + ed.GitLabOrganizationName, ed.AutoEnabled, ed.BranchProtectionEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = data + fmt.Sprintf(" with auto-enabled-cla-group set to %s", ed.AutoEnabledClaGroupID) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GitLabOrganizationDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab group %s was deleted", ed.GitLabOrganizationName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GitLabOrganizationUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := "The GitLab group" // nolint + if ed.GitLabOrganizationName != "" { + data = fmt.Sprintf("%s with name: %s", data, ed.GitLabOrganizationName) + } + if ed.GitLabGroupID > 0 { + data = fmt.Sprintf("%s with group ID: %d", data, ed.GitLabGroupID) } - data = data + fmt.Sprintf(" by: %s.", args.userName) + data = fmt.Sprintf("%s was updated with auto-enabled: %t", data, ed.AutoEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = fmt.Sprintf("%s with auto-enabled-cla-group: %s", data, ed.AutoEnabledClaGroupID) + } + if args.ProjectName != "" { + data = fmt.Sprintf("%s for the project %s", data, args.ProjectName) + } + if args.UserName != "" { + data = fmt.Sprintf("%s by the user %s", data, args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CCLAApprovalListRequestApprovedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s approved a CCLA Approval Request for Project: %s, Company: %s.", - args.userName, args.projectName, args.companyName) + data := fmt.Sprintf("The user %s approved a CCLA approval request", args.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CCLAApprovalListRequestRejectedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s rejected a CCLA Approval Request for Project: %s, Company: %s.", - args.userName, args.projectName, args.companyName) + data := fmt.Sprintf("The user %s rejected a CCLA approval request", args.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAManagerRequestCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s added CLA Manager Request: %s for Company: %s, Project: %s.", - ed.UserName, ed.RequestID, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The user %s added a CLA Manager request", args.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAManagerCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s was added as CLA Manager for Company: %s, Project: %s.", - ed.UserName, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The user %s was added as CLA Manager", ed.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", ed.ProjectName) + } else { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAManagerDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s was removed as CLA Manager for Company: %s, Project: %s.", - ed.UserLFID, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The user %s was removed as CLA Manager", ed.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if ed.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", ed.ProjectName) + } else { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAManagerRequestApprovedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager Request: %s for User: %s was approved by: %s for Company: %s, Project: %s.", - ed.RequestID, ed.UserName, ed.ManagerName, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The CLA Manager request for the user %s was approved by the CLA Manager %s", + ed.UserName, ed.ManagerName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAManagerRequestDeniedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager Request: %s for User: %s was denied by: %s for Company: %s, Project: %s.", - ed.RequestID, ed.UserName, ed.ManagerName, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The CLA Manager request for the user %s was denied by the CLA Manager %s", + ed.UserName, ed.ManagerName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAManagerRequestDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager Request: %s for User: %s was deleted by: %s for Company: %s, Project: %s.", - ed.RequestID, ed.UserName, ed.ManagerName, ed.CompanyName, ed.ProjectName) + data := fmt.Sprintf("The CLA Manager request for the user %s was deleted by the CLA Manager %s", + ed.UserName, ed.ManagerName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListAddEmailData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s added Email: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListEmail, args.companyName, args.projectName) + data := fmt.Sprintf("The email address %s was added to the approval list", ed.ApprovalListEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListRemoveEmailData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s removed Email: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListEmail, args.companyName, args.projectName) + data := fmt.Sprintf("The email address %s was removed from the approval list", ed.ApprovalListEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListAddDomainData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s added Domain: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListDomain, args.companyName, args.projectName) + data := fmt.Sprintf("The email address domain %s was added to the approval list", ed.ApprovalListDomain) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListRemoveDomainData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s removed Domain: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListDomain, args.companyName, args.projectName) + data := fmt.Sprintf("The email address domain %s was removed from the approval list", ed.ApprovalListDomain) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListAddGitHubUsernameData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s added GitHub Username: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListGitHubUsername, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub username %s was added to the approval list", ed.ApprovalListGitHubUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListRemoveGitHubUsernameData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s removed GitHub Username: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListGitHubUsername, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub username %s was removed from the approval list", ed.ApprovalListGitHubUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListAddGitHubOrgData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s added GitHub Organization: %s to the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListGitHubOrg, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub organization %s was added to the approval list", ed.ApprovalListGitHubOrg) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAApprovalListRemoveGitHubOrgData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s removed GitHub Organization: %s from the approval list for Company: %s, Project: %s.", - ed.UserName, ed.ApprovalListGitHubOrg, args.companyName, args.projectName) + data := fmt.Sprintf("The GitHub organization %s was removed from the approval list", ed.ApprovalListGitHubOrg) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event +func (ed *CLAApprovalListAddGitLabUsernameData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab username %s was added to the approval list", ed.ApprovalListGitLabUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *CLAApprovalListRemoveGitLabUsernameData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab username %s was removed from the approval list", ed.ApprovalListGitLabUsername) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *CLAApprovalListAddGitLabGroupData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab group %s was added to the approval list", ed.ApprovalListGitLabGroup) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *CLAApprovalListRemoveGitLabGroupData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The GitLab group %s was removed from the approval list", ed.ApprovalListGitLabGroup) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the CLA Manager %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event func (ed *CCLAApprovalListRequestCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("User: %s created a CCLA Approval Request for Project: %s, Company: %s.", - args.userName, args.projectName, args.companyName) + data := fmt.Sprintf("The user %s created a CCLA Approval Request", args.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ApprovalListGitHubOrganizationAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s added GitHub Organization: %s to the whitelist for Project: %s, Company: %s.", - args.userName, ed.GitHubOrganizationName, args.projectName, args.companyName) + data := fmt.Sprintf("The CLA Manager %s added the GitHub organization %s to the approval list", args.UserName, ed.GitHubOrganizationName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ApprovalListGitHubOrganizationDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("CLA Manager: %s removed GitHub Organization: %s from the whitelist for Project: %s, Company: %s.", - args.userName, ed.GitHubOrganizationName, args.projectName, args.companyName) + data := fmt.Sprintf("The CLA Manager %s removed the GitHub organization %s from the approval list", args.UserName, ed.GitHubOrganizationName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ClaManagerAccessRequestAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s has requested to be CLA Manager for the project %s, the company %s.", - args.userName, ed.ProjectName, ed.CompanyName) + data := fmt.Sprintf("The user %s has requested to be CLA Manager", args.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ClaManagerAccessRequestDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s has deleted a request to be CLA Manager.", - args.userName) + data := fmt.Sprintf("The user %s has deleted a request to be CLA Manager", args.UserName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAGroupCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The CLA Group %s was created by the user %s.", - args.projectName, args.userName) - return data, true + data := fmt.Sprintf("The CLA group %s was created", args.CLAGroupName) + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + return data + ".", true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAGroupUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The CLA Group %s was updated by the user %s.", args.projectName, args.userName) - return data, true + var nameUpdated, descriptionUpdated bool + + data := "The CLA Group" // nolint + if ed.NewClaGroupName != "" && ed.OldClaGroupName != ed.NewClaGroupName { + data = fmt.Sprintf("%s name was updated to '%s'", data, ed.NewClaGroupName) + nameUpdated = true + } + + if ed.NewClaGroupDescription != "" && ed.OldClaGroupDescription != ed.NewClaGroupDescription { + descriptionUpdated = true + if nameUpdated { + data = fmt.Sprintf("%s and the description was updated to '%s'", data, ed.NewClaGroupDescription) + } else { + data = fmt.Sprintf("%s description was updated to '%s'", data, ed.NewClaGroupDescription) + } + } + + //shouldn't happen + if !nameUpdated && !descriptionUpdated { + data = data + " was updated" + } + + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + return data + ".", true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *CLAGroupDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The CLA Group %s was deleted by the user %s.", - args.projectName, args.userName) + data := fmt.Sprintf("The CLA group %s was deleted", args.CLAGroupName) + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GerritProjectDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("%d Gerrit repositories were deleted due to CLA Group/Project %s deletion.", - ed.DeletedCount, args.projectName) - return data, false + data := fmt.Sprintf("%d Gerrit repositories were deleted due to CLA Group/Project deletion", ed.DeletedCount) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GerritAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The Gerrit repository %s was added by: %s.", ed.GerritRepositoryName, args.userName) + data := fmt.Sprintf("The Gerrit repository %s was added", ed.GerritRepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GerritDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The Gerrit repository %s was deleted by %s.", ed.GerritRepositoryName, args.userName) + data := fmt.Sprintf("The Gerrit repository %s was deleted", ed.GerritRepositoryName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GerritUserAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The username %s was add to the gerrit group %s", ed.Username, ed.GroupName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GerritUserRemovedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The username %s was removed from the gerrit group %s", ed.Username, ed.GroupName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *GitHubProjectDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("%d GitHub repositories were deleted due to CLA Group/project %s deletion.", - ed.DeletedCount, args.projectName) + data := fmt.Sprintf("%d GitHub repositories were deleted due to CLA Group/project deletion", + ed.DeletedCount) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *SignatureProjectInvalidatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("%d signatures were invalidated (approved set to false) due to CLA Group/Project %s deletion.", - ed.InvalidatedCount, args.projectName) + data := fmt.Sprintf("%d signatures were invalidated (approved set to false) due to CLA Group/Project %s deletion", + ed.InvalidatedCount, args.ProjectName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *SignatureInvalidatedApprovalRejectionEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + reason := noReason + if ed.Email != "" { + reason = fmt.Sprintf("Email: %s approval removal ", ed.Email) + } else if ed.GHUsername != "" { + reason = fmt.Sprintf("GH Username: %s approval removal ", ed.GHUsername) + } + data := fmt.Sprintf("Signature invalidated by %s (approved set to false) due to %s", utils.GetBestUsername(ed.CLAManager), reason) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ContributorNotifyCompanyAdminData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s notified the company admin %s by the email address %s for the company %s.", - args.userName, ed.AdminName, ed.AdminEmail, args.companyName) + data := fmt.Sprintf("The user %s notified the company admin %s by the email address %s", + args.UserName, ed.AdminName, ed.AdminEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ContributorNotifyCLADesignee) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s notified the CLA Designee %s by email %s for the project: %s and the company %s.", - args.userName, ed.DesigneeName, ed.DesigneeEmail, - args.projectName, args.companyName) + data := fmt.Sprintf("The user %s notified the CLA Designee %s by email %s", args.UserName, ed.DesigneeName, ed.DesigneeEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ContributorAssignCLADesignee) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s was assigned as CLA Manager Designee for the project: %s for the company %s by the user %s.", - ed.DesigneeName, - args.projectName, args.companyName, args.userName) + data := fmt.Sprintf("The user %s was assigned as CLA Manager Designee", ed.DesigneeName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *UserConvertToContactData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s was converted to a contact for the project %s.", - args.LfUsername, args.projectName) + data := fmt.Sprintf("The user '%s' with email '%s' was converted to a contact", ed.UserName, ed.UserEmail) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *AssignRoleScopeData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s was added to the role %s for project: %s.", args.LfUsername, ed.Role, args.projectName) + data := fmt.Sprintf("The user '%s' with email '%s' was added to the role %s", ed.UserName, ed.UserEmail, ed.Role) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, true } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ClaManagerRoleCreatedData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s was added with to the role %s by user %s.", ed.UserName, ed.Role, args.userName) + data := fmt.Sprintf("The user '%s' with email '%s' was added to the role %s", ed.UserName, ed.UserEmail, ed.Role) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." return data, false } -// GetEventSummaryString . . . +// GetEventSummaryString returns the summary string for this event func (ed *ClaManagerRoleDeletedData) GetEventSummaryString(args *LogEventArgs) (string, bool) { - data := fmt.Sprintf("The user %s was removed from the role %s by user %s.", ed.UserName, ed.Role, args.userName) + data := fmt.Sprintf("The user '%s' with email '%s' was added to the role %s", ed.UserName, ed.UserEmail, ed.Role) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, false +} + +// GetEventSummaryString returns the summary string for this event +func (ed *SignatureAutoCreateECLAUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The user %s updated the auto-create ECLA flag to %t", args.LfUsername, ed.AutoCreateECLA) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.CompanyName != "" { + data = data + fmt.Sprintf(" for the company %s", args.CompanyName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, false +} + +func (ed *IndividualSignatureSignedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The user %s signed an individual CLA", args.LfUsername) + if ed.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", ed.ProjectName) + } + if ed.ProjectID != "" { + data = data + fmt.Sprintf(" with project ID: %s", ed.ProjectID) + } + return data + ".", false +} + +func (ed *IndividualSignatureSignedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The user %s signed an individual CLA for project %s", + args.LfUsername, ed.ProjectName) return data, false } diff --git a/cla-backend-go/events/event_data_test.go b/cla-backend-go/events/event_data_test.go new file mode 100644 index 000000000..4f6bd1440 --- /dev/null +++ b/cla-backend-go/events/event_data_test.go @@ -0,0 +1,148 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package events + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testUser = "john" +) + +func TestCLAGroupUpdatedEventData_GetEventSummaryString(t *testing.T) { + + testCases := []struct { + name string + eventData *CLAGroupUpdatedEventData + summaryStr string + }{ + { + name: "empty", + eventData: &CLAGroupUpdatedEventData{}, + summaryStr: "The CLA Group was updated by the user john.", + }, + { + name: "only name updated", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupName: "updatedNameValue", + }, + summaryStr: "The CLA Group name was updated to 'updatedNameValue' by the user john.", + }, + { + name: "only name updated but old description still passed", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupName: "updatedNameValue", + NewClaGroupDescription: "oldDescriptionValue", + OldClaGroupDescription: "oldDescriptionValue", + }, + summaryStr: "The CLA Group name was updated to 'updatedNameValue' by the user john.", + }, + { + name: "only description updated", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupDescription: "updatedDescriptionValue", + }, + summaryStr: "The CLA Group description was updated to 'updatedDescriptionValue' by the user john.", + }, + { + name: "only description updated but old name still passed", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupDescription: "updatedDescriptionValue", + NewClaGroupName: "oldNameValue", + OldClaGroupName: "oldNameValue", + }, + summaryStr: "The CLA Group description was updated to 'updatedDescriptionValue' by the user john.", + }, + { + name: "name and description updated", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupName: "updatedNameValue", + NewClaGroupDescription: "updatedDescriptionValue", + }, + summaryStr: "The CLA Group name was updated to 'updatedNameValue' and the description was updated to 'updatedDescriptionValue' by the user john.", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + summary, _ := tc.eventData.GetEventSummaryString(&LogEventArgs{UserName: testUser}) + assert.Equal(tt, tc.summaryStr, summary) + }) + } +} + +func TestCLAGroupUpdatedEventData_GetEventDetailsString(t *testing.T) { + projectID := "projectIDValue" + + testCases := []struct { + name string + eventData *CLAGroupUpdatedEventData + detailStr string + }{ + { + name: "empty", + eventData: &CLAGroupUpdatedEventData{}, + detailStr: "The CLA Group was updated by the user john.", + }, + { + name: "only name updated", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupName: "updatedNameValue", + OldClaGroupName: "oldNameValue", + }, + detailStr: "The CLA Group name was updated to 'updatedNameValue' by the user john.", + }, + { + name: "only name updated but old description still passed", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupName: "updatedNameValue", + OldClaGroupName: "oldNameValue", + NewClaGroupDescription: "oldDescriptionValue", + OldClaGroupDescription: "oldDescriptionValue", + }, + detailStr: "The CLA Group name was updated to 'updatedNameValue' by the user john.", + }, + { + name: "only description updated", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupDescription: "updatedDescriptionValue", + OldClaGroupDescription: "oldDescriptionValue", + }, + detailStr: "The CLA Group description was updated to 'updatedDescriptionValue' by the user john.", + }, + { + name: "only description updated but old name still passed", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupDescription: "updatedDescriptionValue", + OldClaGroupDescription: "oldDescriptionValue", + NewClaGroupName: "oldNameValue", + OldClaGroupName: "oldNameValue", + }, + detailStr: "The CLA Group description was updated to 'updatedDescriptionValue' by the user john.", + }, + { + name: "name and description updated", + eventData: &CLAGroupUpdatedEventData{ + NewClaGroupName: "updatedNameValue", + OldClaGroupName: "oldNameValue", + NewClaGroupDescription: "updatedDescriptionValue", + OldClaGroupDescription: "oldDescriptionValue", + }, + detailStr: "The CLA Group name was updated to 'updatedNameValue' and the description was updated to 'updatedDescriptionValue' by the user john.", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + summary, _ := tc.eventData.GetEventDetailsString(&LogEventArgs{ + UserName: testUser, + ProjectID: projectID, + }) + assert.Equal(tt, tc.detailStr, summary) + }) + } +} diff --git a/cla-backend-go/events/event_types.go b/cla-backend-go/events/event_types.go index c80300f4a..cc6435a1c 100644 --- a/cla-backend-go/events/event_types.go +++ b/cla-backend-go/events/event_types.go @@ -32,7 +32,10 @@ const ( UserDeleted = "user.deleted" RepositoryAdded = "repository.added" + RepositoryRenamed = "repository.renamed" + RepositoryTransferred = "repository.transferred" RepositoryDisabled = "repository.disabled" + RepositoryDeleted = "repository.deleted" RepositoryUpdated = "repository.updated" RepositoryBranchProtectionAdded = "repository.branchprotection.updated" RepositoryBranchProtectionDisabled = "repository.branchprotection.updated" @@ -40,11 +43,17 @@ const ( GerritRepositoryAdded = "gerrit_repository.added" GerritRepositoryDeleted = "gerrit_repository.deleted" + GerritUserAdded = "gerrit_user.added" + GerritUserRemoved = "gerrit_user.deleted" GitHubOrganizationAdded = "github_organization.added" GitHubOrganizationDeleted = "github_organization.deleted" GitHubOrganizationUpdated = "github_organization.updated" + GitlabOrganizationAdded = "gitlab_organization.added" + GitlabOrganizationDeleted = "gitlab_organization.deleted" + GitlabOrganizationUpdated = "gitlab_organization.updated" + CompanyACLUserAdded = "company_acl.user_added" CompanyACLRequestAdded = "company_acl.request_added" CompanyACLRequestApproved = "company_acl.request_approved" @@ -69,9 +78,11 @@ const ( ClaManagerRoleCreated = "cla_manager.added" ClaManagerRoleDeleted = "cla_manager.deleted" - CLAGroupCreated = "cla_group.created" - CLAGroupUpdated = "cla_group.updated" - CLAGroupDeleted = "cla_group.deleted" + CLAGroupCreated = "cla_group.created" + CLAGroupUpdated = "cla_group.updated" + CLAGroupDeleted = "cla_group.deleted" + CLAGroupEnrolledProject = "cla_group.enrolled.project" + CLAGroupUnenrolledProject = "cla_group.unenrolled.project" InvalidatedSignature = "signature.invalidated" @@ -80,7 +91,12 @@ const ( ContributorAssignCLADesigneeType = "contributor.assign_designee" ConvertUserToContactType = "lfx_user.convert_to_contact" AssignUserRoleScopeType = "lfx_org_service.assign_user_role_scope" + RemoveUserRoleScopeType = "lfx_org_service.remove_user_role_scope" + + ProjectServiceCLAEnabled = "project.service.cla.enabled" + ProjectServiceCLADisabled = "project.service.cla.disabled" + SignatureAutoCreateECLAUpdated = "signature.auto_create_ecla.updated" - ProjectServiceCLAEnabled = "project.service.cla.enabled" - ProjectServiceCLADisabled = "project.service.cla.disabled" + IndividualSignatureSigned = "individual.signature.signed" + CorporateSignatureSigned = "corporate.signature.signed" ) diff --git a/cla-backend-go/events/handlers.go b/cla-backend-go/events/handlers.go index 919f0d3a7..5703673e5 100644 --- a/cla-backend-go/events/handlers.go +++ b/cla-backend-go/events/handlers.go @@ -4,9 +4,9 @@ package events import ( - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/events" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + 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/user" "github.com/go-openapi/runtime/middleware" diff --git a/cla-backend-go/events/mock/mock_repository.go b/cla-backend-go/events/mock/mock_repository.go new file mode 100644 index 000000000..e8691089b --- /dev/null +++ b/cla-backend-go/events/mock/mock_repository.go @@ -0,0 +1,173 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: events/repository.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + events "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/events" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// AddDataToEvent mocks base method. +func (m *MockRepository) AddDataToEvent(eventID, parentProjectSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddDataToEvent", eventID, parentProjectSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddDataToEvent indicates an expected call of AddDataToEvent. +func (mr *MockRepositoryMockRecorder) AddDataToEvent(eventID, parentProjectSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDataToEvent", reflect.TypeOf((*MockRepository)(nil).AddDataToEvent), eventID, parentProjectSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID) +} + +// CreateEvent mocks base method. +func (m *MockRepository) CreateEvent(event *models.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateEvent", event) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateEvent indicates an expected call of CreateEvent. +func (mr *MockRepositoryMockRecorder) CreateEvent(event interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEvent", reflect.TypeOf((*MockRepository)(nil).CreateEvent), event) +} + +// GetClaGroupEvents mocks base method. +func (m *MockRepository) GetClaGroupEvents(claGroupID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupEvents", claGroupID, nextKey, paramPageSize, all, searchTerm) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupEvents indicates an expected call of GetClaGroupEvents. +func (mr *MockRepositoryMockRecorder) GetClaGroupEvents(claGroupID, nextKey, paramPageSize, all, searchTerm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupEvents", reflect.TypeOf((*MockRepository)(nil).GetClaGroupEvents), claGroupID, nextKey, paramPageSize, all, searchTerm) +} + +// GetCompanyClaGroupEvents mocks base method. +func (m *MockRepository) GetCompanyClaGroupEvents(claGroupID, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyClaGroupEvents", claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyClaGroupEvents indicates an expected call of GetCompanyClaGroupEvents. +func (mr *MockRepositoryMockRecorder) GetCompanyClaGroupEvents(claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyClaGroupEvents", reflect.TypeOf((*MockRepository)(nil).GetCompanyClaGroupEvents), claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all) +} + +// GetCompanyEvents mocks base method. +func (m *MockRepository) GetCompanyEvents(companyID, eventType string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyEvents", companyID, eventType, nextKey, paramPageSize, all) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyEvents indicates an expected call of GetCompanyEvents. +func (mr *MockRepositoryMockRecorder) GetCompanyEvents(companyID, eventType, nextKey, paramPageSize, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyEvents", reflect.TypeOf((*MockRepository)(nil).GetCompanyEvents), companyID, eventType, nextKey, paramPageSize, all) +} + +// GetCompanyFoundationEvents mocks base method. +func (m *MockRepository) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyFoundationEvents", companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyFoundationEvents indicates an expected call of GetCompanyFoundationEvents. +func (mr *MockRepositoryMockRecorder) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyFoundationEvents", reflect.TypeOf((*MockRepository)(nil).GetCompanyFoundationEvents), companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all) +} + +// GetFoundationEvents mocks base method. +func (m *MockRepository) GetFoundationEvents(foundationSFID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFoundationEvents", foundationSFID, nextKey, paramPageSize, all, searchTerm) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFoundationEvents indicates an expected call of GetFoundationEvents. +func (mr *MockRepositoryMockRecorder) GetFoundationEvents(foundationSFID, nextKey, paramPageSize, all, searchTerm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoundationEvents", reflect.TypeOf((*MockRepository)(nil).GetFoundationEvents), foundationSFID, nextKey, paramPageSize, all, searchTerm) +} + +// GetRecentEvents mocks base method. +func (m *MockRepository) GetRecentEvents(pageSize int64) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRecentEvents", pageSize) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRecentEvents indicates an expected call of GetRecentEvents. +func (mr *MockRepositoryMockRecorder) GetRecentEvents(pageSize interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecentEvents", reflect.TypeOf((*MockRepository)(nil).GetRecentEvents), pageSize) +} + +// SearchEvents mocks base method. +func (m *MockRepository) SearchEvents(params *events.SearchEventsParams, pageSize int64) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchEvents", params, pageSize) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchEvents indicates an expected call of SearchEvents. +func (mr *MockRepositoryMockRecorder) SearchEvents(params, pageSize interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchEvents", reflect.TypeOf((*MockRepository)(nil).SearchEvents), params, pageSize) +} diff --git a/cla-backend-go/events/mock/mock_service.go b/cla-backend-go/events/mock/mock_service.go new file mode 100644 index 000000000..29dac77ec --- /dev/null +++ b/cla-backend-go/events/mock/mock_service.go @@ -0,0 +1,270 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: events/service.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + events "github.com/communitybridge/easycla/cla-backend-go/events" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + events0 "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/events" + projects_cla_groups "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + 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 +} + +// GetClaGroupEvents mocks base method. +func (m *MockService) GetClaGroupEvents(claGroupID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupEvents", claGroupID, nextKey, paramPageSize, all, searchTerm) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupEvents indicates an expected call of GetClaGroupEvents. +func (mr *MockServiceMockRecorder) GetClaGroupEvents(claGroupID, nextKey, paramPageSize, all, searchTerm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupEvents", reflect.TypeOf((*MockService)(nil).GetClaGroupEvents), claGroupID, nextKey, paramPageSize, all, searchTerm) +} + +// GetCompanyClaGroupEvents mocks base method. +func (m *MockService) GetCompanyClaGroupEvents(claGroupID, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyClaGroupEvents", claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyClaGroupEvents indicates an expected call of GetCompanyClaGroupEvents. +func (mr *MockServiceMockRecorder) GetCompanyClaGroupEvents(claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyClaGroupEvents", reflect.TypeOf((*MockService)(nil).GetCompanyClaGroupEvents), claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all) +} + +// GetCompanyEvents mocks base method. +func (m *MockService) GetCompanyEvents(companyID, eventType string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyEvents", companyID, eventType, nextKey, paramPageSize, all) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyEvents indicates an expected call of GetCompanyEvents. +func (mr *MockServiceMockRecorder) GetCompanyEvents(companyID, eventType, nextKey, paramPageSize, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyEvents", reflect.TypeOf((*MockService)(nil).GetCompanyEvents), companyID, eventType, nextKey, paramPageSize, all) +} + +// GetCompanyFoundationEvents mocks base method. +func (m *MockService) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCompanyFoundationEvents", companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCompanyFoundationEvents indicates an expected call of GetCompanyFoundationEvents. +func (mr *MockServiceMockRecorder) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompanyFoundationEvents", reflect.TypeOf((*MockService)(nil).GetCompanyFoundationEvents), companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all) +} + +// GetFoundationEvents mocks base method. +func (m *MockService) GetFoundationEvents(foundationSFID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFoundationEvents", foundationSFID, nextKey, paramPageSize, all, searchTerm) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFoundationEvents indicates an expected call of GetFoundationEvents. +func (mr *MockServiceMockRecorder) GetFoundationEvents(foundationSFID, nextKey, paramPageSize, all, searchTerm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoundationEvents", reflect.TypeOf((*MockService)(nil).GetFoundationEvents), foundationSFID, nextKey, paramPageSize, all, searchTerm) +} + +// GetRecentEvents mocks base method. +func (m *MockService) GetRecentEvents(paramPageSize *int64) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRecentEvents", paramPageSize) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRecentEvents indicates an expected call of GetRecentEvents. +func (mr *MockServiceMockRecorder) GetRecentEvents(paramPageSize interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecentEvents", reflect.TypeOf((*MockService)(nil).GetRecentEvents), paramPageSize) +} + +// LogEvent mocks base method. +func (m *MockService) LogEvent(args *events.LogEventArgs) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "LogEvent", args) +} + +// LogEvent indicates an expected call of LogEvent. +func (mr *MockServiceMockRecorder) LogEvent(args interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogEvent", reflect.TypeOf((*MockService)(nil).LogEvent), args) +} + +// LogEventWithContext mocks base method. +func (m *MockService) LogEventWithContext(ctx context.Context, args *events.LogEventArgs) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "LogEventWithContext", ctx, args) +} + +// LogEventWithContext indicates an expected call of LogEventWithContext. +func (mr *MockServiceMockRecorder) LogEventWithContext(ctx, args interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogEventWithContext", reflect.TypeOf((*MockService)(nil).LogEventWithContext), ctx, args) +} + +// SearchEvents mocks base method. +func (m *MockService) SearchEvents(params *events0.SearchEventsParams) (*models.EventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchEvents", params) + ret0, _ := ret[0].(*models.EventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchEvents indicates an expected call of SearchEvents. +func (mr *MockServiceMockRecorder) SearchEvents(params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchEvents", reflect.TypeOf((*MockService)(nil).SearchEvents), params) +} + +// MockCombinedRepo is a mock of CombinedRepo interface. +type MockCombinedRepo struct { + ctrl *gomock.Controller + recorder *MockCombinedRepoMockRecorder +} + +// MockCombinedRepoMockRecorder is the mock recorder for MockCombinedRepo. +type MockCombinedRepoMockRecorder struct { + mock *MockCombinedRepo +} + +// NewMockCombinedRepo creates a new mock instance. +func NewMockCombinedRepo(ctrl *gomock.Controller) *MockCombinedRepo { + mock := &MockCombinedRepo{ctrl: ctrl} + mock.recorder = &MockCombinedRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCombinedRepo) EXPECT() *MockCombinedRepoMockRecorder { + return m.recorder +} + +// GetCLAGroupByID mocks base method. +func (m *MockCombinedRepo) GetCLAGroupByID(ctx context.Context, claGroupID string, loadRepoDetails bool) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupByID", ctx, claGroupID, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupByID indicates an expected call of GetCLAGroupByID. +func (mr *MockCombinedRepoMockRecorder) GetCLAGroupByID(ctx, claGroupID, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupByID", reflect.TypeOf((*MockCombinedRepo)(nil).GetCLAGroupByID), ctx, claGroupID, loadRepoDetails) +} + +// GetClaGroupIDForProject mocks base method. +func (m *MockCombinedRepo) GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*projects_cla_groups.ProjectClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupIDForProject", ctx, projectSFID) + ret0, _ := ret[0].(*projects_cla_groups.ProjectClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupIDForProject indicates an expected call of GetClaGroupIDForProject. +func (mr *MockCombinedRepoMockRecorder) GetClaGroupIDForProject(ctx, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupIDForProject", reflect.TypeOf((*MockCombinedRepo)(nil).GetClaGroupIDForProject), ctx, projectSFID) +} + +// GetCompany mocks base method. +func (m *MockCombinedRepo) 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 *MockCombinedRepoMockRecorder) GetCompany(ctx, companyID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCompany", reflect.TypeOf((*MockCombinedRepo)(nil).GetCompany), ctx, companyID) +} + +// GetUser mocks base method. +func (m *MockCombinedRepo) 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 *MockCombinedRepoMockRecorder) GetUser(userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockCombinedRepo)(nil).GetUser), userID) +} + +// GetUserByUserName mocks base method. +func (m *MockCombinedRepo) 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 *MockCombinedRepoMockRecorder) GetUserByUserName(userName, fullMatch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUserName", reflect.TypeOf((*MockCombinedRepo)(nil).GetUserByUserName), userName, fullMatch) +} diff --git a/cla-backend-go/events/mockrepo.go b/cla-backend-go/events/mockrepo.go index f74ae1373..e023f0871 100644 --- a/cla-backend-go/events/mockrepo.go +++ b/cla-backend-go/events/mockrepo.go @@ -7,23 +7,38 @@ import ( "context" "time" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/events" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/events" "github.com/go-openapi/strfmt" ) // mockRepository data model type mockRepository struct{} -func (repo *mockRepository) AddDataToEvent(eventID, foundationSFID, projectSFID, projectSFName, companySFID, projectID string) error { +// GetEventsByType implements Repository. +func (repo *mockRepository) GetEventsByType(eventType string, pageSize int64) ([]*models.Event, error) { + panic("unimplemented") +} + +// GetCCLAEvents implements Repository. +func (repo *mockRepository) GetCCLAEvents(claGroupId string, companyID string, searchTerm string, eventType string, pageSize int64) ([]*models.Event, error) { + panic("unimplemented") +} + +func (repo *mockRepository) AddDataToEvent(eventID, foundationSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID string) error { panic("implement me") } -func (repo *mockRepository) GetCompanyFoundationEvents(companySFID, foundationSFID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { +func (repo *mockRepository) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchterm *string, all bool) (*models.EventList, error) { panic("implement me") } -func (repo *mockRepository) GetCompanyClaGroupEvents(companySFID, claGroupID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { +func (repo *mockRepository) GetCompanyClaGroupEvents(claGroupIDs string, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { panic("implement me") } @@ -39,10 +54,51 @@ func (repo *mockRepository) GetClaGroupEvents(claGroupID string, nextKey *string panic("implement me") } +func (repo *mockRepository) GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*projects_cla_groups.ProjectClaGroup, error) { + return nil, nil +} + +func (repo *mockRepository) LogEvent(args *LogEventArgs) { + repo.LogEventWithContext(utils.NewContext(), args) +} + +func (repo *mockRepository) LogEventWithContext(ctx context.Context, args *LogEventArgs) { + event := models.Event{ + EventType: args.EventType, + + UserID: args.UserID, + UserName: args.UserName, + LfUsername: args.LfUsername, + + EventCLAGroupID: args.CLAGroupID, + EventCLAGroupName: args.CLAGroupName, + + EventCompanyID: args.CompanyID, + EventCompanySFID: args.CompanySFID, + EventCompanyName: args.CompanyName, + + EventProjectID: args.ProjectID, + EventProjectSFID: args.ProjectSFID, + EventProjectName: args.ProjectName, + EventParentProjectSFID: args.ParentProjectSFID, + EventParentProjectName: args.ParentProjectName, + + //EventData: eventData, + //EventSummary: eventSummary, + + //ContainsPII: containsPII, + } + + err := repo.CreateEvent(&event) + if err != nil { + log.WithError(err).Warn("unable to create event") + } +} + var events []*models.Event // NewMockRepository creates a new instance of the mock event repository -func NewMockRepository() *mockRepository { +func NewMockRepository() *mockRepository { // nolint return &mockRepository{} } diff --git a/cla-backend-go/events/models.go b/cla-backend-go/events/models.go index fa05cc407..291a10cf5 100644 --- a/cla-backend-go/events/models.go +++ b/cla-backend-go/events/models.go @@ -3,31 +3,39 @@ package events -import "github.com/communitybridge/easycla/cla-backend-go/gen/models" +import "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" -//IndividualSignedEvent represntative of ICLA signatures +// IndividualSignedEvent represntative of ICLA signatures const IndividualSignedEvent = "IndividualSignatureSigned" // Event data model type Event struct { - EventID string `dynamodbav:"event_id"` - EventType string `dynamodbav:"event_type"` - EventUserID string `dynamodbav:"event_user_id"` - EventUserName string `dynamodbav:"event_user_name"` - EventLfUsername string `dynamodbav:"event_lf_username"` - EventProjectID string `dynamodbav:"event_project_id"` - EventProjectExternalID string `dynamodbav:"event_project_external_id"` - EventProjectName string `dynamodbav:"event_project_name"` - EventCompanyID string `dynamodbav:"event_company_id"` - EventCompanyName string `dynamodbav:"event_company_name"` - EventTime string `dynamodbav:"event_time"` - EventTimeEpoch int64 `dynamodbav:"event_time_epoch"` - EventData string `dynamodbav:"event_data"` - EventSummary string `dynamodbav:"event_summary"` - EventFoundationSFID string `dynamodbav:"event_foundation_sfid"` - EventSFProjectName string `dynamodbav:"event_sf_project_name"` + EventID string `dynamodbav:"event_id"` + EventType string `dynamodbav:"event_type"` + + EventUserID string `dynamodbav:"event_user_id"` + EventUserName string `dynamodbav:"event_user_name"` + EventLfUsername string `dynamodbav:"event_lf_username"` + + EventCLAGroupID string `dynamodbav:"event_cla_group_id"` + EventCLAGroupName string `dynamodbav:"event_cla_group_name"` + EventCLAGroupNameLower string `dynamodbav:"event_cla_group_name_lower"` + + EventProjectID string `dynamodbav:"event_project_id"` // legacy, same as the SFID EventProjectSFID string `dynamodbav:"event_project_sfid"` - EventCompanySFID string `dynamodbav:"event_company_sfid"` + EventProjectName string `dynamodbav:"event_project_name"` + EventParentProjectSFID string `dynamodbav:"event_parent_project_sfid"` + EventParentProjectName string `dynamodbav:"event_parent_project_name"` + + EventCompanyID string `dynamodbav:"event_company_id"` + EventCompanySFID string `dynamodbav:"event_company_sfid"` + EventCompanyName string `dynamodbav:"event_company_name"` + + EventData string `dynamodbav:"event_data"` + EventSummary string `dynamodbav:"event_summary"` + + EventTime string `dynamodbav:"event_time"` + EventTimeEpoch int64 `dynamodbav:"event_time_epoch"` } // DBUser data model @@ -50,22 +58,32 @@ type DBUser struct { func (e *Event) toEvent() *models.Event { //nolint event := &models.Event{ - EventData: e.EventData, - EventSummary: e.EventSummary, - EventID: e.EventID, + EventID: e.EventID, + EventType: e.EventType, + + UserID: e.EventUserID, + UserName: e.EventUserName, + LfUsername: e.EventLfUsername, + + EventCLAGroupID: e.EventCLAGroupID, + EventCLAGroupName: e.EventCLAGroupName, + EventCLAGroupNameLower: e.EventCLAGroupNameLower, + EventProjectID: e.EventProjectID, - EventProjectExternalID: e.EventProjectExternalID, - EventProjectName: e.EventProjectName, - EventTime: e.EventTime, - EventType: e.EventType, - UserID: e.EventUserID, - UserName: e.EventUserName, - LfUsername: e.EventLfUsername, - EventTimeEpoch: e.EventTimeEpoch, - EventFoundationSFID: e.EventFoundationSFID, EventProjectSFID: e.EventProjectSFID, - EventProjectSFName: e.EventSFProjectName, - EventCompanySFID: e.EventCompanySFID, + EventProjectName: e.EventProjectName, + EventParentProjectSFID: e.EventParentProjectSFID, + EventParentProjectName: e.EventParentProjectName, + + EventCompanyID: e.EventCompanyID, + EventCompanySFID: e.EventCompanySFID, + EventCompanyName: e.EventCompanyName, + + EventTime: e.EventTime, + EventTimeEpoch: e.EventTimeEpoch, + + EventData: e.EventData, + EventSummary: e.EventSummary, } // Disregard Company details for ICLA event if event.EventType != IndividualSignedEvent { diff --git a/cla-backend-go/events/repository.go b/cla-backend-go/events/repository.go index e0204a371..281be25bf 100644 --- a/cla-backend-go/events/repository.go +++ b/cla-backend-go/events/repository.go @@ -4,10 +4,12 @@ package events import ( + "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" + "math/big" "strconv" "strings" "time" @@ -27,8 +29,9 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/events" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/events" ) // errors @@ -39,11 +42,16 @@ var ( // indexes const ( - CompanySFIDFoundationSFIDEpochIndex = "company-sfid-foundation-sfid-event-time-epoch-index" - CompanySFIDProjectIDEpochIndex = "company-sfid-project-id-event-time-epoch-index" - CompanyIDEventTypeIndex = "company-id-event-type-index" - EventFoundationSFIDEpochIndex = "event-foundation-sfid-event-time-epoch-index" - EventProjectIDEpochIndex = "event-project-id-event-time-epoch-index" + CompanySFIDFoundationSFIDEpochIndex = "company-sfid-foundation-sfid-event-time-epoch-index" + CompanySFIDProjectIDEpochIndex = "company-sfid-project-id-event-time-epoch-index" + CompanyIDEventTypeIndex = "company-id-event-type-index" + EventFoundationSFIDEpochIndex = "event-foundation-sfid-event-time-epoch-index" + EventProjectIDEpochIndex = "event-project-id-event-time-epoch-index" + EventCLAGroupIDEpochIndex = "event-cla-group-id-event-time-epoch-index" + EventCompanySFIDEventDataLowerIndex = "event-company-sfid-event-data-lower-index" + CompanyIDExternalProjectIDEventEpochTimeIndex = "company-id-external-project-id-event-epoch-time-index" + CompanySFIDClaGroupIDEpochIndex = "company-sfid-cla-group-id-event-time-epoch-index" + EventProjectSFIDEventTypeIndex = "event-project-sfid-event-type-index" ) // constants @@ -55,12 +63,14 @@ const ( // Repository interface defines methods of event repository service type Repository interface { CreateEvent(event *models.Event) error - AddDataToEvent(eventID, foundationSFID, projectSFID, projectSFName, companySFID, projectID string) error + AddDataToEvent(eventID, parentProjectSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID string) error SearchEvents(params *eventOps.SearchEventsParams, pageSize int64) (*models.EventList, error) + GetCCLAEvents(claGroupId, companyID, searchTerm, eventType string, pageSize int64) ([]*models.Event, error) GetRecentEvents(pageSize int64) (*models.EventList, error) + GetEventsByType(eventType string, pageSize int64) ([]*models.Event, error) - GetCompanyFoundationEvents(companySFID, foundationSFID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) - GetCompanyClaGroupEvents(companySFID, claGroupID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) + GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) + GetCompanyClaGroupEvents(claGroupID string, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) GetCompanyEvents(companyID, eventType string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) GetFoundationEvents(foundationSFID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) GetClaGroupEvents(claGroupID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) @@ -70,6 +80,7 @@ type Repository interface { type repository struct { stage string dynamoDBClient *dynamodb.DynamoDB + eventsTable string } // NewRepository creates a new instance of the event repository @@ -77,6 +88,7 @@ func NewRepository(awsSession *session.Session, stage string) Repository { return &repository{ stage: stage, dynamoDBClient: dynamodb.New(awsSession), + eventsTable: fmt.Sprintf("cla-%s-events", stage), } } @@ -85,8 +97,12 @@ func toDateFormat(t time.Time) string { return t.Format("02-01-2006") } -// Create event will create event in database. +// CreateEvent event will create event in database. func (repo *repository) CreateEvent(event *models.Event) error { + f := logrus.Fields{ + "functionName": "v1.events.repository.CreateEvent", + } + if event.UserID == "" { return ErrUserIDRequired } @@ -95,47 +111,65 @@ func (repo *repository) CreateEvent(event *models.Event) error { } eventID, err := uuid.NewV4() if err != nil { - log.Warnf("Unable to generate a UUID for a whitelist request, error: %v", err) + log.WithFields(f).WithError(err).Warnf("Unable to generate a UUID for a whitelist request, error: %v", err) return err } currentTime, currentTimeString := utils.CurrentTime() input := &dynamodb.PutItemInput{ Item: map[string]*dynamodb.AttributeValue{}, - TableName: aws.String(fmt.Sprintf("cla-%s-events", repo.stage)), + TableName: aws.String(repo.eventsTable), } + eventDateAndContainsPII := fmt.Sprintf("%s#%t", toDateFormat(currentTime), event.ContainsPII) addAttribute(input.Item, "event_id", eventID.String()) addAttribute(input.Item, "event_type", event.EventType) + addAttribute(input.Item, "event_user_id", event.UserID) addAttribute(input.Item, "event_user_name", event.UserName) - addAttribute(input.Item, "event_lf_username", event.LfUsername) addAttribute(input.Item, "event_user_name_lower", strings.ToLower(event.UserName)) - addAttribute(input.Item, "event_time", currentTimeString) - addAttribute(input.Item, "event_data", event.EventData) - addAttribute(input.Item, "event_summary", event.EventSummary) + addAttribute(input.Item, "event_lf_username", event.LfUsername) + addAttribute(input.Item, "event_company_id", event.EventCompanyID) + addAttribute(input.Item, "event_company_sfid", event.EventCompanySFID) addAttribute(input.Item, "event_company_name", event.EventCompanyName) addAttribute(input.Item, "event_company_name_lower", strings.ToLower(event.EventCompanyName)) + + addAttribute(input.Item, "event_cla_group_id", event.EventCLAGroupID) + addAttribute(input.Item, "event_cla_group_name", event.EventCLAGroupName) + addAttribute(input.Item, "event_cla_group_name_lower", strings.ToLower(event.EventCLAGroupName)) + addAttribute(input.Item, "event_project_id", event.EventProjectID) + addAttribute(input.Item, "event_project_sfid", event.EventProjectSFID) addAttribute(input.Item, "event_project_name", event.EventProjectName) addAttribute(input.Item, "event_project_name_lower", strings.ToLower(event.EventProjectName)) + addAttribute(input.Item, "event_parent_project_sfid", event.EventParentProjectSFID) + addAttribute(input.Item, "event_parent_project_name", event.EventParentProjectName) + + addAttribute(input.Item, "event_data", event.EventData) + // For filtering/searching + addAttribute(input.Item, "event_data_lower", strings.ToLower(event.EventData)) + addAttribute(input.Item, "event_summary", event.EventSummary) + + addAttribute(input.Item, "event_time", currentTimeString) addAttribute(input.Item, "event_date", toDateFormat(currentTime)) - addAttribute(input.Item, "event_project_external_id", event.EventProjectExternalID) addAttribute(input.Item, "event_date_and_contains_pii", eventDateAndContainsPII) + addAttribute(input.Item, "date_created", toDateFormat(currentTime)) + addAttribute(input.Item, "date_modified", toDateFormat(currentTime)) + input.Item["contains_pii"] = &dynamodb.AttributeValue{BOOL: &event.ContainsPII} input.Item["event_time_epoch"] = &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(currentTime.Unix(), 10))} - if event.EventCompanyID != "" && event.EventProjectExternalID != "" { - companyIDexternalProjectID := fmt.Sprintf("%s#%s", event.EventCompanyID, event.EventProjectExternalID) - addAttribute(input.Item, "company_id_external_project_id", companyIDexternalProjectID) + if event.EventCompanyID != "" && event.EventProjectSFID != "" { + companyIDExternalProjectID := fmt.Sprintf("%s#%s", event.EventCompanyID, event.EventProjectSFID) + addAttribute(input.Item, "company_id_external_project_id", companyIDExternalProjectID) } _, err = repo.dynamoDBClient.PutItem(input) if err != nil { - log.Warnf("Unable to create a new event, error: %v", err) + log.WithFields(f).WithError(err).Warnf("Unable to create a new event, error: %v", err) return err } - log.Printf("added event : %s", eventID.String()) + log.WithFields(f).Infof("added event ID: %s of type: %s", eventID.String(), event.EventType) return nil } @@ -193,7 +227,7 @@ func createSearchEventFilter(pk string, sk string, params *eventOps.SearchEvents filter = addConditionToFilter(filter, filterExpression, &filterAdded) } if params.SearchTerm != nil { - filterExpression := expression.Name("event_data").Contains(*params.SearchTerm) + filterExpression := expression.Name("event_data_lower").Contains(strings.ToLower(*params.SearchTerm)) filter = addConditionToFilter(filter, filterExpression, &filterAdded) } if filterAdded { @@ -219,16 +253,218 @@ func addTimeExpression(keyCond expression.KeyConditionBuilder, params *eventOps. return keyCond } +func isProvisionedThroughputExceeded(err error) bool { + f := logrus.Fields{ + "functionName": "v1.events.repository.isProvisionedThroughputExceeded", + } + + if err == nil { + return false + } + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == "ProvisionedThroughputExceededException" { + log.WithFields(f).WithError(err).Warn("provisioned throughput exceeded") + return true + } + + log.WithFields(f).WithError(err).Warn("error checking for provisioned throughput exceeded") + return false +} + +func exponentialBackoffSleep(retry int) { + // Base delay + baseDelay := 100 + + // Mx backoff time + maxBackoff := 20000 + + // Calculate delay + delay := baseDelay * (1 << retry) + + if delay > maxBackoff { + delay = maxBackoff + } + + // Add jitter + jitter, err := rand.Int(rand.Reader, big.NewInt(int64(delay))) + if err != nil { + log.Warnf("error generating random number: %v", err) + return + } + totalDelay := time.Duration(int64(delay)+jitter.Int64()) * time.Millisecond + + time.Sleep(totalDelay) +} + +// GetEvents +func (repo *repository) GetCCLAEvents(claGroupId, companyID, searchTerm, eventType string, pageSize int64) ([]*models.Event, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.GetCCLAEvents", + "claGroupId": claGroupId, + "companyID": companyID, + "eventType": eventType, + "pageSize": pageSize, + } + + log.WithFields(f).Debug("querying events table...") + condition := expression.Key("event_cla_group_id").Equal(expression.Value(claGroupId)) + builder := expression.NewBuilder().WithKeyCondition(condition) + + filter := expression.Name("event_company_id").Equal(expression.Value(companyID)). + And(expression.Name("event_type").Equal(expression.Value(eventType))) + + if searchTerm != "" { + filter = filter.And(expression.Name("event_data_lower").Contains(strings.ToLower(searchTerm))) + } + + builder = builder.WithFilter(filter) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.eventsTable), + IndexName: aws.String(EventCLAGroupIDEpochIndex), + Limit: aws.Int64(pageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) + } + + events := make([]*models.Event, 0) + + var results *dynamodb.QueryOutput + + maxRetries := 5 + + for retry := 0; retry < maxRetries; retry++ { + // Perform the query... + log.WithFields(f).Debugf("retrying query: %d", retry) + var errQuery error + results, errQuery = repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + if retry == maxRetries || isProvisionedThroughputExceeded(errQuery) { + log.WithFields(f).WithError(errQuery).Warn("error retrieving events") + return nil, errQuery + } + log.WithFields(f).WithError(errQuery).Warn("error retrieving events - retrying...") + exponentialBackoffSleep(retry) + continue + } + + // Build the result models + eventsList, modelErr := buildEventListModels(results) + if modelErr != nil { + log.WithFields(f).WithError(modelErr).Warn("error convert event list models") + return nil, modelErr + } + + events = append(events, eventsList...) + log.WithFields(f).Debugf("loaded %d events", len(events)) + + // We have more records if last evaluated key has a value + log.WithFields(f).Debugf("last evaluated key %+v", results.LastEvaluatedKey) + if len(results.LastEvaluatedKey) > 0 { + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + retry = 0 + } else { + break + } + } + + return events, nil + +} + +func (repo *repository) GetEventsByType(eventType string, pageSize int64) ([]*models.Event, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.GetEventsByType", + "eventType": eventType, + "pageSize": pageSize, + } + + log.WithFields(f).Debug("querying events table...") + condition := expression.Key("event_type").Equal(expression.Value(eventType)) + + builder := expression.NewBuilder().WithKeyCondition(condition) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.eventsTable), + IndexName: aws.String("event-type-index"), + Limit: aws.Int64(pageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) + } + + events := make([]*models.Event, 0) + + var results *dynamodb.QueryOutput + + maxRetries := 3 + + for retry := 0; retry < maxRetries; retry++ { + // Perform the query... + log.WithFields(f).Debugf("retrying query: %d", retry) + var errQuery error + results, errQuery = repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + if retry == maxRetries || isProvisionedThroughputExceeded(errQuery) { + log.WithFields(f).WithError(errQuery).Warn("error retrieving events") + return nil, errQuery + } + log.WithFields(f).WithError(errQuery).Warn("error retrieving events - retrying...") + exponentialBackoffSleep(retry) + continue + } + + // Build the result models + eventsList, modelErr := buildEventListModels(results) + if modelErr != nil { + log.WithFields(f).WithError(modelErr).Warn("error convert event list models") + return nil, modelErr + } + + events = append(events, eventsList...) + log.WithFields(f).Debugf("loaded %d events", len(events)) + + // We have more records if last evaluated key has a value + if len(results.LastEvaluatedKey) > 0 { + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + retry = 0 + } else { + break + } + } + return events, nil +} + // SearchEvents returns list of events matching with filter criteria. func (repo *repository) SearchEvents(params *eventOps.SearchEventsParams, pageSize int64) (*models.EventList, error) { - if params.ProjectID == nil { - return nil, errors.New("invalid request. projectID is compulsory") + f := logrus.Fields{ + "functionName": "v1.events.repository.SearchEvents", + "pageSize": pageSize, + } + + if params.ProjectID == nil && params.ProjectSFID == nil { + return nil, errors.New("invalid request. projectID|projectSFID is compulsory") } var condition expression.KeyConditionBuilder var indexName, pk, sk string builder := expression.NewBuilder().WithProjection(buildProjection()) - // The table we're interested in - tableName := fmt.Sprintf("cla-%s-events", repo.stage) switch { case params.ProjectID != nil: @@ -238,7 +474,14 @@ func (repo *repository) SearchEvents(params *eventOps.SearchEventsParams, pageSi pk = "event_project_id" condition = addTimeExpression(condition, params) sk = "event_time_epoch" + case params.ProjectSFID != nil: + // search by projectSFID + indexName = EventProjectSFIDEventTypeIndex + condition = expression.Key("event_project_sfid").Equal(expression.Value(params.ProjectSFID)).And(expression.Key("event_type").Equal(expression.Value(params.EventType))) + pk = "event_project_sfid" + sk = "event_type" } + filter := createSearchEventFilter(pk, sk, params) if filter != nil { builder = builder.WithFilter(*filter) @@ -257,7 +500,7 @@ func (repo *repository) SearchEvents(params *eventOps.SearchEventsParams, pageSi KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), FilterExpression: expr.Filter(), - TableName: aws.String(tableName), + TableName: aws.String(repo.eventsTable), IndexName: aws.String(indexName), Limit: aws.Int64(pageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) } @@ -266,67 +509,94 @@ func (repo *repository) SearchEvents(params *eventOps.SearchEventsParams, pageSi } if params.NextKey != nil { - log.Debugf("Received a nextKey, value: %s", *params.NextKey) - queryInput.ExclusiveStartKey, err = fromString(*params.NextKey) + queryInput.ExclusiveStartKey, err = decodeNextKey(*params.NextKey) if err != nil { + log.WithFields(f).WithError(err).Warn("problem decoding next key value") return nil, err } + log.WithFields(f).Debugf("received a nextKey, value: %s - decoded: %+v", *params.NextKey, queryInput.ExclusiveStartKey) } - var lastEvaluatedKey string events := make([]*models.Event, 0) + var results *dynamodb.QueryOutput - for ok := true; ok; ok = lastEvaluatedKey != "" { - results, errQuery := repo.dynamoDBClient.Query(queryInput) + for { + // Perform the query... + var errQuery error + results, errQuery = repo.dynamoDBClient.Query(queryInput) if errQuery != nil { - log.Warnf("error retrieving events. error = %s", errQuery.Error()) + log.WithFields(f).WithError(errQuery).Warnf("error retrieving events") return nil, errQuery } + // Build the result models eventsList, modelErr := buildEventListModels(results) if modelErr != nil { + log.WithFields(f).WithError(modelErr).Warn("error convert event list models") return nil, modelErr } + // Trim to how many the caller asked for - just in case we go over events = append(events, eventsList...) - if len(results.LastEvaluatedKey) != 0 { - queryInput.ExclusiveStartKey = results.LastEvaluatedKey + if int64(len(events)) > pageSize { + events = events[:pageSize] } - lastEvaluatedKey, err = toString(results.LastEvaluatedKey) - if err != nil { - return nil, err + log.WithFields(f).Debugf("loaded %d events", len(events)) + + // We have more records if last evaluated key has a value + log.WithFields(f).Debugf("last evaluated key %+v", results.LastEvaluatedKey) + if len(results.LastEvaluatedKey) > 0 { + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + break } + if int64(len(events)) >= pageSize { break } } - return &models.EventList{ - Events: events, - NextKey: lastEvaluatedKey, - }, nil + response := &models.EventList{ + Events: events, + } + + log.WithFields(f).Debugf("returning %d events - last key: %+v", len(events), results.LastEvaluatedKey) + if len(results.LastEvaluatedKey) > 0 { + log.WithFields(f).Debug("building next key...") + encodedString, err := buildNextKey(indexName, events[len(events)-1]) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to build nextKey") + } + response.NextKey = encodedString + log.WithFields(f).Debugf("lastEvaluatedKey encoded is: %s", encodedString) + } + + return response, nil } // queryEventsTable queries events table on index -func (repo *repository) queryEventsTable(indexName string, condition expression.KeyConditionBuilder, nextKey *string, pageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { +func (repo *repository) queryEventsTable(indexName string, condition expression.KeyConditionBuilder, filter *expression.ConditionBuilder, nextKey *string, pageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { f := logrus.Fields{ - "functionName": "events.queryEventsTable", + "functionName": "v1.events.repository.queryEventsTable", "indexName": indexName, - "nextKey": aws.StringValue(nextKey), - "pageSize": aws.Int64Value(pageSize), - "all": all, - "searchTerm": aws.StringValue(searchTerm), + //"nextKey": aws.StringValue(nextKey), + "pageSize": aws.Int64Value(pageSize), + "all": all, + "searchTerm": aws.StringValue(searchTerm), } - log.WithFields(f).Debug("querying events table") - builder := expression.NewBuilder() // .WithProjection(buildProjection()) - // The table we're interested in - tableName := fmt.Sprintf("cla-%s-events", repo.stage) + log.WithFields(f).Debug("querying events table...") + builder := expression.NewBuilder().WithKeyCondition(condition) + + // Add the filter to the builder, if we have one + if filter != nil { + builder.WithFilter(*filter) + } - builder = builder.WithKeyCondition(condition) // Use the nice builder to create the expression expr, err := builder.Build() if err != nil { + log.WithFields(f).WithError(err).Warn("problem building events query") return nil, err } @@ -336,113 +606,131 @@ func (repo *repository) queryEventsTable(indexName string, condition expression. ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), - FilterExpression: expr.Filter(), - TableName: aws.String(tableName), + TableName: aws.String(repo.eventsTable), IndexName: aws.String(indexName), - ScanIndexForward: aws.Bool(false), + ScanIndexForward: aws.Bool(false), // Specifies the order for index traversal: If true (default), the traversal is performed in ascending order; if false, the traversal is performed in descending order. } - if all { - pageSize = aws.Int64(HugePageSize) - } else { - if pageSize == nil { - pageSize = aws.Int64(DefaultPageSize) - } + // Add the filter expression if we have one + if filter != nil { + queryInput.FilterExpression = expr.Filter() } - if searchTerm != nil { - // since we are filtering data in client side, we should use large pageSize to avoid recursive query + if all { queryInput.Limit = aws.Int64(HugePageSize) } else { - queryInput.Limit = aws.Int64(*pageSize + 1) + if pageSize == nil { + queryInput.Limit = aws.Int64(DefaultPageSize) + } else { + if *pageSize > HugePageSize { + queryInput.Limit = aws.Int64(HugePageSize) + } + queryInput.Limit = pageSize + } } + maxResults := *queryInput.Limit - if nextKey != nil && !all { - // log.Debugf("Received a nextKey, value: %s", *nextKey) - queryInput.ExclusiveStartKey, err = fromString(*nextKey) + // If we have the next key, set the exclusive start key value + if nextKey != nil { + queryInput.ExclusiveStartKey, err = decodeNextKey(*nextKey) if err != nil { + log.WithFields(f).WithError(err).Warn("problem decoding next key value") return nil, err } + log.WithFields(f).Debugf("received a nextKey, value: %s - decoded: %+v", *nextKey, queryInput.ExclusiveStartKey) } - // log.WithField("queryInput", *queryInput).Debug("query") - var lastEvaluatedKey string events := make([]*models.Event, 0) + var results *dynamodb.QueryOutput - if searchTerm != nil { - searchTerm = aws.String(strings.ToLower(*searchTerm)) - } - - for ok := true; ok; ok = lastEvaluatedKey != "" { - results, errQuery := repo.dynamoDBClient.Query(queryInput) + for { + // Perform the query... + var errQuery error + log.WithFields(f).Debugf("queryInput: %+v", queryInput) + results, errQuery = repo.dynamoDBClient.Query(queryInput) if errQuery != nil { - log.WithFields(f).WithError(errQuery).Warnf("error retrieving events. error = %s", errQuery.Error()) + log.WithFields(f).WithError(errQuery).Warn("error retrieving events") return nil, errQuery } + // Build the result models eventsList, modelErr := buildEventListModels(results) if modelErr != nil { + log.WithFields(f).WithError(modelErr).Warn("error convert event list models") return nil, modelErr } - if searchTerm != nil { - for _, event := range eventsList { - if !all { - if int64(len(events)) >= (*pageSize + 1) { - break - } - } - if strings.Contains(strings.ToLower(event.EventData), *searchTerm) { - events = append(events, event) - } - } - } else { - events = append(events, eventsList...) + + events = append(events, eventsList...) + // Add search term filtering + if len(events) > 0 && searchTerm != nil { + log.WithFields(f).Debugf("filtering events by search term: %s", *searchTerm) + events = filterEventsBySearchTerm(events, *searchTerm) } - if len(results.LastEvaluatedKey) != 0 { + // Trim to how many the caller asked for - just in case we go over + if int64(len(events)) > maxResults { + events = events[:maxResults] + } + log.WithFields(f).Debugf("loaded %d events", len(events)) + + // We have more records if last evaluated key has a value + log.WithFields(f).Debugf("last evaluated key %+v", results.LastEvaluatedKey) + if len(results.LastEvaluatedKey) > 0 { queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { break } - if !all { - if int64(len(events)) >= (*pageSize + 1) { - break - } + if int64(len(events)) >= maxResults { + break } } - if !all { - if int64(len(events)) > *pageSize { - events = events[0:*pageSize] - lastEvaluatedKey, err = buildNextKey(indexName, events[*pageSize-1]) + + if len(events) > 0 { + response := &models.EventList{ + Events: events, + ResultCount: int64(len(events)), + } + log.WithFields(f).Debugf("returning %d events - last key: %+v", len(events), results.LastEvaluatedKey) + if len(results.LastEvaluatedKey) > 0 { + log.WithFields(f).Debug("building next key...") + encodedString, err := buildNextKey(indexName, events[len(events)-1]) if err != nil { - log.WithFields(f).WithError(err).Warnf("unable to build nextKey. index = %s, event = %#v error = %s", indexName, events[*pageSize-1], err.Error()) + log.WithFields(f).WithError(err).Warn("unable to build nextKey") } - } else { - events = events[0:int64(len(events))] + response.NextKey = encodedString + log.WithFields(f).Debugf("lastEvaluatedKey encoded is: %s", encodedString) } - } - if len(events) > 0 { - return &models.EventList{ - Events: events, - NextKey: lastEvaluatedKey, - }, nil + return response, nil } // Just return an empty response - no events - just an empty list, and no nextKey return &models.EventList{ - Events: []*models.Event{}, + Events: []*models.Event{}, + ResultCount: 0, }, nil } +func filterEventsBySearchTerm(events []*models.Event, s string) []*models.Event { + var filteredEvents []*models.Event + for _, event := range events { + log.Debugf("checking event: %s", event.EventData) + if strings.Contains(strings.ToLower(event.EventData), strings.ToLower(s)) { + filteredEvents = append(filteredEvents, event) + } + } + return filteredEvents +} + func buildNextKey(indexName string, event *models.Event) (string, error) { nextKey := make(map[string]*dynamodb.AttributeValue) nextKey["event_id"] = &dynamodb.AttributeValue{S: aws.String(event.EventID)} switch indexName { case CompanySFIDFoundationSFIDEpochIndex: nextKey["company_sfid_foundation_sfid"] = &dynamodb.AttributeValue{ - S: aws.String(fmt.Sprintf("%s#%s", event.EventCompanySFID, event.EventFoundationSFID)), + S: aws.String(fmt.Sprintf("%s#%s", event.EventCompanySFID, event.EventParentProjectSFID)), } nextKey["event_time_epoch"] = &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(event.EventTimeEpoch, 10))} case CompanySFIDProjectIDEpochIndex: @@ -451,7 +739,7 @@ func buildNextKey(indexName string, event *models.Event) (string, error) { } nextKey["event_time_epoch"] = &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(event.EventTimeEpoch, 10))} case EventFoundationSFIDEpochIndex: - nextKey["event_foundation_sfid"] = &dynamodb.AttributeValue{S: aws.String(event.EventFoundationSFID)} + nextKey["event_parent_project_sfid"] = &dynamodb.AttributeValue{S: aws.String(event.EventParentProjectSFID)} nextKey["event_time_epoch"] = &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(event.EventTimeEpoch, 10))} case EventProjectIDEpochIndex: nextKey["event_project_id"] = &dynamodb.AttributeValue{S: aws.String(event.EventProjectID)} @@ -459,46 +747,105 @@ func buildNextKey(indexName string, event *models.Event) (string, error) { case CompanyIDEventTypeIndex: nextKey["company_id"] = &dynamodb.AttributeValue{S: aws.String(event.EventCompanyID)} nextKey["event_type"] = &dynamodb.AttributeValue{S: aws.String(event.EventType)} + case EventCLAGroupIDEpochIndex: + nextKey["event_cla_group_id"] = &dynamodb.AttributeValue{S: aws.String(event.EventCLAGroupID)} + nextKey["event_time_epoch"] = &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(event.EventTimeEpoch, 10))} + case CompanyIDExternalProjectIDEventEpochTimeIndex: + nextKey["company_id_external_project_id"] = &dynamodb.AttributeValue{ + S: aws.String(fmt.Sprintf("%s#%s", event.EventCompanyID, event.EventProjectSFID)), + } + case CompanySFIDClaGroupIDEpochIndex: + nextKey["company_sfid_cla_group_id"] = &dynamodb.AttributeValue{ + S: aws.String(fmt.Sprintf("%s#%s", event.EventCompanySFID, event.EventCLAGroupID)), + } + nextKey["event_time_epoch"] = &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(event.EventTimeEpoch, 10))} } - return toString(nextKey) + + return encodeNextKey(nextKey) } // GetCompanyFoundationEvents returns the list of events for foundation and company -func (repo *repository) GetCompanyFoundationEvents(companySFID, foundationSFID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { - key := fmt.Sprintf("%s#%s", companySFID, foundationSFID) - keyCondition := expression.Key("company_sfid_foundation_sfid").Equal(expression.Value(key)) - return repo.queryEventsTable(CompanySFIDFoundationSFIDEpochIndex, keyCondition, nextKey, paramPageSize, all, nil) +func (repo *repository) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.GetCompanyFoundationEvents", + "companySFID": companySFID, + "companyID": companyID, + "foundationSFID": foundationSFID, + "nextKey": utils.StringValue(nextKey), + "paramPageSize": utils.Int64Value(paramPageSize), + "loadAll": all, + } + log.WithFields(f).Debugf("adding key condition of 'event_parent_project_sfid = %s'", foundationSFID) + keyCondition := expression.Key("event_parent_project_sfid").Equal(expression.Value(foundationSFID)) + var filter expression.ConditionBuilder + log.WithFields(f).Debugf("adding filter condition of 'event_company_sfid = %s'", companySFID) + filter = expression.Name("event_company_sfid").Equal(expression.Value(companySFID)) + return repo.queryEventsTable(EventFoundationSFIDEpochIndex, keyCondition, &filter, nextKey, paramPageSize, all, searchTerm) } // GetCompanyClaGroupEvents returns the list of events for cla group and the company -func (repo *repository) GetCompanyClaGroupEvents(companySFID, claGroupID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { - key := fmt.Sprintf("%s#%s", companySFID, claGroupID) - keyCondition := expression.Key("company_sfid_project_id").Equal(expression.Value(key)) - return repo.queryEventsTable(CompanySFIDProjectIDEpochIndex, keyCondition, nextKey, paramPageSize, all, nil) +func (repo *repository) GetCompanyClaGroupEvents(claGroupID string, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.GetCompanyClaGroupEvents", + "claGroupID": claGroupID, + "companySFID": companySFID, + "nextKey": utils.StringValue(nextKey), + "paramPageSize": utils.Int64Value(paramPageSize), + "loadAll": all, + } + log.WithFields(f).Debugf("adding key condition of 'company_sfid_cla_group_id = %s'", fmt.Sprintf("%s#%s", companySFID, claGroupID)) + keyCondition := expression.Key("company_sfid_cla_group_id").Equal(expression.Value(fmt.Sprintf("%s#%s", companySFID, claGroupID))) + return repo.queryEventsTable(CompanySFIDClaGroupIDEpochIndex, keyCondition, nil, nextKey, paramPageSize, all, searchTerm) } // GetCompanyEvents returns the list of events for given company id and event types func (repo *repository) GetCompanyEvents(companyID, eventType string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.GetCompanyEvents", + "companyID": companyID, + "nextKey": utils.StringValue(nextKey), + "paramPageSize": utils.Int64Value(paramPageSize), + "loadAll": all, + } + log.WithFields(f).Debugf("adding key condition of 'company_id = %s'", companyID) keyCondition := expression.Key("company_id").Equal(expression.Value(companyID)).And( expression.Key("event_type").Equal(expression.Value(eventType))) - return repo.queryEventsTable(CompanyIDEventTypeIndex, keyCondition, nextKey, paramPageSize, all, nil) + return repo.queryEventsTable(CompanyIDEventTypeIndex, keyCondition, nil, nextKey, paramPageSize, all, nil) } // GetFoundationEvents returns the list of foundation events func (repo *repository) GetFoundationEvents(foundationSFID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { - keyCondition := expression.Key("event_foundation_sfid").Equal(expression.Value(foundationSFID)) - return repo.queryEventsTable(EventFoundationSFIDEpochIndex, keyCondition, nextKey, paramPageSize, all, searchTerm) + f := logrus.Fields{ + "functionName": "v1.events.repository.GetFoundationEvents", + "foundationSFID": foundationSFID, + "nextKey": utils.StringValue(nextKey), + "paramPageSize": utils.Int64Value(paramPageSize), + "loadAll": all, + "searchTerm": utils.StringValue(searchTerm), + } + log.WithFields(f).Debugf("adding key condition of 'event_parent_project_sfid = %s'", foundationSFID) + keyCondition := expression.Key("event_parent_project_sfid").Equal(expression.Value(foundationSFID)) + return repo.queryEventsTable(EventFoundationSFIDEpochIndex, keyCondition, nil, nextKey, paramPageSize, all, searchTerm) } // GetClaGroupEvents returns the list of cla-group events func (repo *repository) GetClaGroupEvents(claGroupID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) { - keyCondition := expression.Key("event_project_id").Equal(expression.Value(claGroupID)) - return repo.queryEventsTable(EventProjectIDEpochIndex, keyCondition, nextKey, paramPageSize, all, searchTerm) + f := logrus.Fields{ + "functionName": "v1.events.repository.GetClaGroupEvents", + "claGroupID": claGroupID, + "nextKey": utils.StringValue(nextKey), + "paramPageSize": utils.Int64Value(paramPageSize), + "loadAll": all, + "searchTerm": utils.StringValue(searchTerm), + } + log.WithFields(f).Debugf("adding key condition of 'event_cla_group_id = %s'", claGroupID) + keyCondition := expression.Key("event_cla_group_id").Equal(expression.Value(claGroupID)) + return repo.queryEventsTable(EventCLAGroupIDEpochIndex, keyCondition, nil, nextKey, paramPageSize, all, searchTerm) } -// toString encodes the map as a string -func toString(in map[string]*dynamodb.AttributeValue) (string, error) { +// encodeNextKey encodes the map as a string +func encodeNextKey(in map[string]*dynamodb.AttributeValue) (string, error) { if len(in) == 0 { return "", nil } @@ -509,35 +856,47 @@ func toString(in map[string]*dynamodb.AttributeValue) (string, error) { return base64.StdEncoding.EncodeToString(b), nil } -// fromString converts the string to a map -func fromString(str string) (map[string]*dynamodb.AttributeValue, error) { +// decodeNextKey decodes the next key value into a dynamodb attribute value +func decodeNextKey(str string) (map[string]*dynamodb.AttributeValue, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.decodeNextKey", + } + sDec, err := base64.StdEncoding.DecodeString(str) if err != nil { + log.WithFields(f).WithError(err).Warnf("error decoding string %s", str) return nil, err } + var m map[string]*dynamodb.AttributeValue err = json.Unmarshal(sDec, &m) if err != nil { + log.WithFields(f).WithError(err).Warnf("error unmarshalling string after decoding: %s", sDec) return nil, err } + return m, nil } // buildEventListModel converts the query results to a list event models func buildEventListModels(results *dynamodb.QueryOutput) ([]*models.Event, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.buildEventListModels", + } events := make([]*models.Event, 0) var items []Event err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &items) if err != nil { - log.Warnf("error unmarshalling events from database, error: %v", - err) + log.WithFields(f).WithError(err).Warn("error unmarshalling events from database") return nil, err } + for _, e := range items { events = append(events, e.toEvent()) } + return events, nil } @@ -562,7 +921,12 @@ func buildProjection() expression.ProjectionBuilder { ) } -func (repo repository) GetRecentEvents(pageSize int64) (*models.EventList, error) { +func (repo *repository) GetRecentEvents(pageSize int64) (*models.EventList, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.GetRecentEvents", + "pageSize": pageSize, + } + ctime := time.Now() maxQueryDays := 30 events := make([]*models.Event, 0) @@ -570,6 +934,7 @@ func (repo repository) GetRecentEvents(pageSize int64) (*models.EventList, error day := toDateFormat(ctime) eventList, err := repo.getEventByDay(day, false, pageSize) if err != nil { + log.WithFields(f).WithError(err).Warn("error fetching events by day") return nil, err } events = append(events, eventList...) @@ -583,11 +948,16 @@ func (repo repository) GetRecentEvents(pageSize int64) (*models.EventList, error return &models.EventList{ Events: events, }, nil - } -func (repo repository) getEventByDay(day string, containsPII bool, pageSize int64) ([]*models.Event, error) { - tableName := fmt.Sprintf("cla-%s-events", repo.stage) +func (repo *repository) getEventByDay(day string, containsPII bool, pageSize int64) ([]*models.Event, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.getEventByDay", + "day": day, + "containsPII": containsPII, + "pageSize": pageSize, + } + var condition expression.KeyConditionBuilder builder := expression.NewBuilder().WithProjection(buildProjection()) @@ -600,8 +970,10 @@ func (repo repository) getEventByDay(day string, containsPII bool, pageSize int6 // Use the nice builder to create the expression expr, err := builder.Build() if err != nil { + log.WithFields(f).WithError(err).Warn("error building events query") return nil, err } + // Assemble the query input parameters queryInput := &dynamodb.QueryInput{ ExpressionAttributeNames: expr.Names(), @@ -609,7 +981,7 @@ func (repo repository) getEventByDay(day string, containsPII bool, pageSize int6 KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), FilterExpression: expr.Filter(), - TableName: aws.String(tableName), + TableName: aws.String(repo.eventsTable), IndexName: aws.String(indexName), Limit: aws.Int64(pageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) ScanIndexForward: aws.Bool(false), @@ -620,12 +992,13 @@ func (repo repository) getEventByDay(day string, containsPII bool, pageSize int6 for { results, errQuery := repo.dynamoDBClient.Query(queryInput) if errQuery != nil { - log.Warnf("error retrieving events. error = %s", errQuery.Error()) + log.WithFields(f).Warnf("error retrieving events. error = %s", errQuery.Error()) return nil, errQuery } eventsList, modelErr := buildEventListModels(results) if modelErr != nil { + log.WithFields(f).Warn("error building event models") return nil, modelErr } @@ -644,41 +1017,60 @@ func (repo repository) getEventByDay(day string, containsPII bool, pageSize int6 return events, nil } -func (repo repository) AddDataToEvent(eventID, foundationSFID, projectSFID, projectSFName, companySFID, projectID string) error { - tableName := fmt.Sprintf("cla-%s-events", repo.stage) +func (repo *repository) AddDataToEvent(eventID, parentProjectSFID, projectSFID, projectSFName, companySFID, projectID, claGroupID string) error { + f := logrus.Fields{ + "functionName": "v1.events.repository.AddDataToEvent", + "eventID": eventID, + "parentProjectSFID": parentProjectSFID, + "projectSFID": projectSFID, + "projectSFName": projectSFName, + "companySFID": companySFID, + "projectID": projectID, + "claGroupID": claGroupID, + } + input := &dynamodb.UpdateItemInput{ - TableName: aws.String(tableName), + TableName: aws.String(repo.eventsTable), Key: map[string]*dynamodb.AttributeValue{ "event_id": { S: aws.String(eventID), }, }, } - companySFIDFoundationSFID := fmt.Sprintf("%s#%s", companySFID, foundationSFID) + companySFIDFoundationSFID := fmt.Sprintf("%s#%s", companySFID, parentProjectSFID) companySFIDProjectID := fmt.Sprintf("%s#%s", companySFID, projectID) + companySFIDClaGroupID := fmt.Sprintf("%s#%s", companySFID, claGroupID) + ue := utils.NewDynamoUpdateExpression() - ue.AddAttributeName("#foundation_sfid", "event_foundation_sfid", foundationSFID != "") + ue.AddAttributeName("#foundation_sfid", "foundation_sfid", parentProjectSFID != "") ue.AddAttributeName("#project_sfid", "event_project_sfid", projectSFID != "") ue.AddAttributeName("#project_sf_name", "event_sf_project_name", projectSFName != "") + ue.AddAttributeName("#company_sfid", "event_company_sfid", companySFID != "") - ue.AddAttributeName("#company_sfid_foundation_sfid", "company_sfid_foundation_sfid", companySFID != "" && foundationSFID != "") + ue.AddAttributeName("#company_sfid_foundation_sfid", "company_sfid_foundation_sfid", companySFID != "" && parentProjectSFID != "") ue.AddAttributeName("#company_sfid_project_id", "company_sfid_project_id", companySFID != "" && projectID != "") + ue.AddAttributeName("#company_sfid_cla_group_id", "company_sfid_cla_group_id", companySFID != "" && claGroupID != "") - ue.AddAttributeValue(":foundation_sfid", &dynamodb.AttributeValue{S: aws.String(foundationSFID)}, foundationSFID != "") + ue.AddAttributeValue(":foundation_sfid", &dynamodb.AttributeValue{S: aws.String(parentProjectSFID)}, parentProjectSFID != "") ue.AddAttributeValue(":project_sfid", &dynamodb.AttributeValue{S: aws.String(projectSFID)}, projectSFID != "") ue.AddAttributeValue(":project_sf_name", &dynamodb.AttributeValue{S: aws.String(projectSFName)}, projectSFName != "") + ue.AddAttributeValue(":company_sfid", &dynamodb.AttributeValue{S: aws.String(companySFID)}, companySFID != "") - ue.AddAttributeValue(":company_sfid_foundation_sfid", &dynamodb.AttributeValue{S: aws.String(companySFIDFoundationSFID)}, companySFID != "" && foundationSFID != "") + ue.AddAttributeValue(":company_sfid_foundation_sfid", &dynamodb.AttributeValue{S: aws.String(companySFIDFoundationSFID)}, companySFID != "" && parentProjectSFID != "") ue.AddAttributeValue(":company_sfid_project_id", &dynamodb.AttributeValue{S: aws.String(companySFIDProjectID)}, companySFID != "" && projectID != "") + ue.AddAttributeValue(":company_sfid_cla_group_id", &dynamodb.AttributeValue{S: aws.String(companySFIDClaGroupID)}, companySFID != "" && claGroupID != "") - ue.AddUpdateExpression("#foundation_sfid = :foundation_sfid", foundationSFID != "") + ue.AddUpdateExpression("#foundation_sfid = :foundation_sfid", parentProjectSFID != "") ue.AddUpdateExpression("#project_sfid = :project_sfid", projectSFID != "") ue.AddUpdateExpression("#project_sf_name = :project_sf_name", projectSFName != "") + ue.AddUpdateExpression("#company_sfid = :company_sfid", companySFID != "") - ue.AddUpdateExpression("#company_sfid_foundation_sfid = :company_sfid_foundation_sfid", companySFID != "" && foundationSFID != "") + ue.AddUpdateExpression("#company_sfid_foundation_sfid = :company_sfid_foundation_sfid", companySFID != "" && parentProjectSFID != "") ue.AddUpdateExpression("#company_sfid_project_id = :company_sfid_project_id", companySFID != "" && projectID != "") + ue.AddUpdateExpression("#company_sfid_cla_group_id = :company_sfid_cla_group_id", companySFID != "" && claGroupID != "") if ue.Expression == "" { // nothing to update + log.WithFields(f).Warn("not expression - nothing to update") return nil } input.UpdateExpression = aws.String(ue.Expression) @@ -686,9 +1078,9 @@ func (repo repository) AddDataToEvent(eventID, foundationSFID, projectSFID, proj input.ExpressionAttributeValues = ue.ExpressionAttributeValues _, updateErr := repo.dynamoDBClient.UpdateItem(input) if updateErr != nil { - log.Debugf("update input: %v", input) - log.Warnf("unable to add extra details to event : %s . error = %s", eventID, updateErr.Error()) + log.WithFields(f).WithError(updateErr).Warnf("unable to add extra details to event : %s . error = %s", eventID, updateErr.Error()) return updateErr } + return nil } diff --git a/cla-backend-go/events/service.go b/cla-backend-go/events/service.go index 622fdf056..bf53cd785 100644 --- a/cla-backend-go/events/service.go +++ b/cla-backend-go/events/service.go @@ -8,10 +8,18 @@ import ( "errors" "fmt" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + + project_service "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + user_service "github.com/communitybridge/easycla/cla-backend-go/v2/user-service" + userServiceModels "github.com/communitybridge/easycla/cla-backend-go/v2/user-service/models" + + "github.com/sirupsen/logrus" + "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - eventOps "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/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" ) @@ -25,13 +33,14 @@ const ( // Service interface defines methods of event service type Service interface { LogEvent(args *LogEventArgs) + LogEventWithContext(ctx context.Context, args *LogEventArgs) SearchEvents(params *eventOps.SearchEventsParams) (*models.EventList, error) GetRecentEvents(paramPageSize *int64) (*models.EventList, error) GetFoundationEvents(foundationSFID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) GetClaGroupEvents(claGroupID string, nextKey *string, paramPageSize *int64, all bool, searchTerm *string) (*models.EventList, error) - GetCompanyFoundationEvents(companySFID, foundationSFID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) - GetCompanyClaGroupEvents(companySFID, claGroupID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) + GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) + GetCompanyClaGroupEvents(claGroupID string, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) GetCompanyEvents(companyID, eventType string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) } @@ -41,6 +50,7 @@ type CombinedRepo interface { GetCompany(ctx context.Context, companyID string) (*models.Company, error) GetUserByUserName(userName string, fullMatch bool) (*models.User, error) GetUser(userID string) (*models.User, error) + GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*projects_cla_groups.ProjectClaGroup, error) } type service struct { @@ -91,13 +101,13 @@ func (s *service) GetClaGroupEvents(projectSFDC string, nextKey *string, paramPa } // GetCompanyFoundationEvents returns list of events for company and foundation -func (s *service) GetCompanyFoundationEvents(companySFID, foundationSFID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { - return s.repo.GetCompanyFoundationEvents(companySFID, foundationSFID, nextKey, paramPageSize, all) +func (s *service) GetCompanyFoundationEvents(companySFID, companyID, foundationSFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + return s.repo.GetCompanyFoundationEvents(companySFID, companyID, foundationSFID, nextKey, paramPageSize, searchTerm, all) } // GetCompanyClaGroupEvents returns list of events for company and cla group -func (s *service) GetCompanyClaGroupEvents(companySFID, claGroupID string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { - return s.repo.GetCompanyClaGroupEvents(companySFID, claGroupID, nextKey, paramPageSize, all) +func (s *service) GetCompanyClaGroupEvents(claGroupID string, companySFID string, nextKey *string, paramPageSize *int64, searchTerm *string, all bool) (*models.EventList, error) { + return s.repo.GetCompanyClaGroupEvents(claGroupID, companySFID, nextKey, paramPageSize, searchTerm, all) } func (s *service) GetCompanyEvents(companyID, eventType string, nextKey *string, paramPageSize *int64, all bool) (*models.EventList, error) { @@ -108,143 +118,359 @@ func (s *service) GetCompanyEvents(companyID, eventType string, nextKey *string, // EventType, EventData are compulsory. // One of LfUsername, UserID must be present type LogEventArgs struct { - EventType string - ProjectID string - ClaGroupModel *models.ClaGroup - CompanyID string - CompanyModel *models.Company - LfUsername string - UserID string - UserModel *models.User - ExternalProjectID string - EventData EventData - userName string - projectName string - companyName string + EventType string + + UserID string + LfUsername string + UserName string + UserModel *models.User + LFUser *userServiceModels.User + + CLAGroupID string + CLAGroupName string + ClaGroupModel *models.ClaGroup + + ProjectID string // Should just use CLA GroupID + ProjectSFID string + ProjectName string + ParentProjectSFID string + ParentProjectName string + + CompanyID string + CompanyName string + CompanySFID string + CompanyModel *models.Company + + EventData EventData } func (s *service) loadCompany(ctx context.Context, args *LogEventArgs) error { + f := logrus.Fields{ + "functionName": "v1.events.service.loadCompany", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if args == nil { + return errors.New("unable to load company data - args is nil") + } + if args.CompanyModel != nil { - args.companyName = args.CompanyModel.CompanyName + args.CompanyName = args.CompanyModel.CompanyName args.CompanyID = args.CompanyModel.CompanyID + args.CompanySFID = args.CompanyModel.CompanyExternalID return nil - } - if args.CompanyID != "" { + } else if args.CompanyID != "" { companyModel, err := s.combinedRepo.GetCompany(ctx, args.CompanyID) if err != nil { + log.WithFields(f).WithError(err).Warnf("failed to load company record ID: %s", args.CompanyID) return err } args.CompanyModel = companyModel - args.companyName = companyModel.CompanyName + args.CompanyName = companyModel.CompanyName + args.CompanySFID = companyModel.CompanyExternalID } + return nil } -func (s *service) loadProject(ctx context.Context, args *LogEventArgs) error { +func (s *service) loadCLAGroup(ctx context.Context, args *LogEventArgs) error { + f := logrus.Fields{ + "functionName": "v1.events.service.loadCLAGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if args == nil { + return errors.New("unable to load CLA Group data - args is nil") + } + + // First, attempt to user the CLA Group model that was provided... if args.ClaGroupModel != nil { - args.ProjectID = args.ClaGroupModel.ProjectID - args.projectName = args.ClaGroupModel.ProjectName - args.ExternalProjectID = args.ClaGroupModel.ProjectExternalID - return nil + args.CLAGroupID = args.ClaGroupModel.ProjectID + args.CLAGroupName = args.ClaGroupModel.ProjectName + } else { + // Did they set the CLA Group ID? + var claGroupID string + if args.CLAGroupID != "" { + claGroupID = args.CLAGroupID + } else if args.ProjectID != "" && utils.IsUUIDv4(args.ProjectID) { // legacy parameter + claGroupID = args.ProjectID + } + + // Load the CLA Group ID if set... + if claGroupID != "" { + claGroupModel, err := s.combinedRepo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) + if err != nil { + log.WithFields(f).WithError(err).Warnf("failed to load CLA Group by ID: %s", claGroupID) + return err + } + args.ClaGroupModel = claGroupModel + args.CLAGroupName = claGroupModel.ProjectName + args.CLAGroupID = claGroupID + } else if args.ProjectSFID != "" { + projectCLAGroupModel, projectCLAGroupErr := s.combinedRepo.GetClaGroupIDForProject(ctx, args.ProjectSFID) + if projectCLAGroupErr != nil || projectCLAGroupModel == nil { + log.WithFields(f).WithError(projectCLAGroupErr).Warnf("failed to load project CLA Group mapping by SFID: %s", args.ProjectSFID) + return nil + } + + args.CLAGroupID = projectCLAGroupModel.ClaGroupID + args.CLAGroupName = projectCLAGroupModel.ClaGroupName + } } - if args.ProjectID != "" { - claGroupModel, err := s.combinedRepo.GetCLAGroupByID(ctx, args.ProjectID, DontLoadRepoDetails) - if err != nil { - return err + + return nil +} + +func (s *service) loadSFProject(ctx context.Context, args *LogEventArgs) error { + if args == nil { + return errors.New("unable to load SF project data - args is nil") + } + + f := logrus.Fields{ + "functionName": "v1.events.service.loadSFProject", + "projectID": args.ProjectID, + "projectSFID": args.ProjectSFID, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + // if it's a legacy model (v1) we need ot set the project sfid from project id + if args.ProjectSFID == "" && args.ProjectID != "" && utils.IsSalesForceID(args.ProjectID) { + args.ProjectSFID = args.ProjectID + } + + // if project sfid not there try to set it from claGroupModel (v2) + if args.ProjectSFID == "" && args.ClaGroupModel != nil && args.ClaGroupModel.ProjectExternalID != "" { + args.ProjectSFID = args.ClaGroupModel.ProjectExternalID + } + + if args.ProjectSFID != "" && utils.IsSalesForceID(args.ProjectSFID) { + // Check if project exists in platform project service + //log.WithFields(f).Debugf("loading salesforce project by ID: %s...", args.ProjectSFID) + project, projectErr := project_service.GetClient().GetProject(args.ProjectSFID) + if projectErr != nil || project == nil { + log.WithFields(f).Warnf("failed to load salesforce project by ID: %s", args.ProjectSFID) + return nil + } + //log.WithFields(f).Debugf("loaded salesforce project by ID: %s", args.ProjectSFID) + args.ProjectName = project.Name + + // Try to load and set the parent information + if utils.IsProjectHaveParent(project) { + //log.WithFields(f).Debugf("loading project parent by ID: %s...", utils.GetProjectParentSFID(project)) + parentProjectModel, parentProjectErr := project_service.GetClient().GetParentProjectModel(project.ID) + if parentProjectErr != nil || parentProjectModel == nil { + log.WithFields(f).Warnf("failed to load project parent by ID: %s", utils.GetProjectParentSFID(project)) + return nil + } + + var parentProjectName, parentProjectID string + if !utils.IsProjectHasRootParent(project) { + parentProjectName = parentProjectModel.Name + parentProjectID = parentProjectModel.ID + } else { + parentProjectName = project.Name + parentProjectID = project.ID + } + //log.WithFields(f).Debugf("loaded project by parent ID: %s - resulting in ID: %s with name: %s", + // project.Foundation.ID, parentProjectID, parentProjectName) + args.ParentProjectSFID = parentProjectID + args.ParentProjectName = parentProjectName + } else { + // No parent, just use the current project as the parent + args.ParentProjectSFID = project.ID + args.ParentProjectName = project.Name } - args.ClaGroupModel = claGroupModel - args.projectName = claGroupModel.ProjectName - args.ExternalProjectID = claGroupModel.ProjectExternalID + } else { + log.WithFields(f).Warnf("project sfid %s was not set properly can't set parent project fields in event", args.ProjectSFID) } + return nil } -func (s *service) loadUser(args *LogEventArgs) error { +func (s *service) loadLFUser(ctx context.Context, args *LogEventArgs) error { + f := logrus.Fields{ + "functionName": "v1.events.service.LFUser", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if args == nil { + return errors.New("unable to load lf user data - args is nil") + } + + if args.LfUsername != "" { + lfUser, lfErr := user_service.GetClient().GetUserByUsername(args.LfUsername) + if lfErr != nil || lfUser == nil { + log.WithFields(f).Warnf("unable to fetch user by username: %s ", args.LfUsername) + return nil + } + args.LFUser = lfUser + } + return nil +} + +func (s *service) loadUser(ctx context.Context, args *LogEventArgs) error { + f := logrus.Fields{ + "functionName": "v1.events.service.loadUser", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + if args == nil { + return errors.New("unable to load user data - args is nil") + } + if args.UserModel != nil { - args.userName = args.UserModel.Username + args.UserName = args.UserModel.Username args.UserID = args.UserModel.UserID args.LfUsername = args.UserModel.LfUsername + log.WithFields(f).Debug("loaded user for event log by caller provided user model") return nil - } - if args.UserID == "" && args.LfUsername == "" { + } else if args.UserID == "" && args.LfUsername == "" { + log.WithFields(f).Warn("failed to load user for event log - user ID and username were not set") return errors.New("require userID or LfUsername") } + var userModel *models.User var err error + // Try loading by LF username if args.LfUsername != "" { + log.WithFields(f).Debugf("loading user by LF username: %s...", args.LfUsername) userModel, err = s.combinedRepo.GetUserByUserName(args.LfUsername, true) if err != nil { - return err + log.WithFields(f).WithError(err).Warnf("failed to load user by username: %s", args.LfUsername) } } + + // Try loading by user ID if args.UserID != "" { + log.WithFields(f).Debugf("loading user by user ID: %s...", args.UserID) userModel, err = s.combinedRepo.GetUser(args.UserID) if err != nil { - return err + log.WithFields(f).WithError(err).Warnf("failed to load user by ID: %s", args.UserID) } } + // Did we finally load the user model? if userModel != nil { args.UserModel = userModel - args.userName = userModel.Username + // Update username with LF Name value if exists ... + if args.LFUser != nil { + args.UserName = args.LFUser.Name + } else { + args.UserName = userModel.Username + } args.UserID = userModel.UserID args.LfUsername = userModel.LfUsername + } else { + log.WithFields(f).Warnf("unable to set user information for event log entry") } return nil } +// loadDetails fetches and sets additional information into the data model required to fill out the event log entry func (s *service) loadDetails(ctx context.Context, args *LogEventArgs) error { + f := logrus.Fields{ + "functionName": "v1.events.service.loadDetails", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + err := s.loadCompany(ctx, args) if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load company details...") + return err + } + + err = s.loadSFProject(ctx, args) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load SF project details...") return err } - err = s.loadProject(ctx, args) + + err = s.loadCLAGroup(ctx, args) if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load CLA Group details...") return err } - err = s.loadUser(args) + + err = s.loadLFUser(ctx, args) if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load LF User details...") return err } + + err = s.loadUser(ctx, args) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load user details...") + return err + } + + err = s.loadLFUser(ctx, args) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load LF user details...") + return err + } + return nil } -// LogEvent logs the event in database -func (s *service) LogEvent(args *LogEventArgs) { - ctx := utils.NewContext() +// LogEventWithContext logs the event in database +func (s *service) LogEventWithContext(ctx context.Context, args *LogEventArgs) { + f := logrus.Fields{ + "functionName": "events.service.LogEventWithContext", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + defer func() { if r := recover(); r != nil { - log.Error("panic occurred in CreateEvent", fmt.Errorf("%v", r)) + log.WithFields(f).Errorf("panic occurred - %+v", r) } }() + if args == nil || args.EventType == "" || args.EventData == nil || (args.UserID == "" && args.LfUsername == "") { - log.Warnf("invalid arguments to LogEvent, missing one or more required values. args %#v", args) + log.WithFields(f).Warnf("invalid arguments to LogEvent, missing one or more required values. Need EventType, EventData or one of UserID or LfUsername. args %#v", args) return } + err := s.loadDetails(ctx, args) if err != nil { - log.Error("unable to load details for event", err) + log.WithFields(f).Error("unable to load details for event", err) return } + eventData, containsPII := args.EventData.GetEventDetailsString(args) eventSummary, _ := args.EventData.GetEventSummaryString(args) event := models.Event{ - ContainsPII: containsPII, - EventCompanyID: args.CompanyID, - EventCompanyName: args.companyName, - EventData: eventData, - EventSummary: eventSummary, - EventProjectExternalID: args.ExternalProjectID, + EventType: args.EventType, + + UserID: args.UserID, + UserName: args.UserName, + LfUsername: args.LfUsername, + + EventCLAGroupID: args.CLAGroupID, + EventCLAGroupName: args.CLAGroupName, + + EventCompanyID: args.CompanyID, + EventCompanySFID: args.CompanySFID, + EventCompanyName: args.CompanyName, + EventProjectID: args.ProjectID, - EventProjectName: args.projectName, - EventType: args.EventType, - UserID: args.UserID, - UserName: args.userName, - LfUsername: args.LfUsername, + EventProjectSFID: args.ProjectSFID, + EventProjectName: args.ProjectName, + EventParentProjectSFID: args.ParentProjectSFID, + EventParentProjectName: args.ParentProjectName, + + EventData: eventData, + EventSummary: eventSummary, + + ContainsPII: containsPII, } err = s.repo.CreateEvent(&event) if err != nil { - log.Error(fmt.Sprintf("unable to create event for args %#v", args), err) + log.WithFields(f).Error(fmt.Sprintf("unable to create event for args %#v", args), err) } } + +// LogEvent logs the event in database +func (s *service) LogEvent(args *LogEventArgs) { + s.LogEventWithContext(utils.NewContext(), args) +} diff --git a/cla-backend-go/gerrits/handlers.go b/cla-backend-go/gerrits/handlers.go index d82043782..015c0fd8f 100644 --- a/cla-backend-go/gerrits/handlers.go +++ b/cla-backend-go/gerrits/handlers.go @@ -8,9 +8,9 @@ import ( "strings" "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/gerrits" + "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/gerrits" "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/go-openapi/runtime/middleware" @@ -52,7 +52,7 @@ func Configure(api *operations.ClaAPI, service Service, projectService ProjectSe return gerrits.NewDeleteGerritBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } // record the event - eventService.LogEvent(&events.LogEventArgs{ + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.GerritRepositoryDeleted, ClaGroupModel: claGroupModel, UserID: claUser.UserID, @@ -92,7 +92,7 @@ func Configure(api *operations.ClaAPI, service Service, projectService ProjectSe return gerrits.NewAddGerritBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } // record the event - eventService.LogEvent(&events.LogEventArgs{ + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.GerritRepositoryAdded, ClaGroupModel: claGroupModel, UserID: claUser.UserID, diff --git a/cla-backend-go/gerrits/lf_group.go b/cla-backend-go/gerrits/lf_group.go index 687401e66..ae9c455fe 100644 --- a/cla-backend-go/gerrits/lf_group.go +++ b/cla-backend-go/gerrits/lf_group.go @@ -5,24 +5,36 @@ package gerrits import ( "bytes" + "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "time" + + v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + + "github.com/LF-Engineering/lfx-kit/auth" + "github.com/communitybridge/easycla/cla-backend-go/events" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" ) // constants const ( DefaultHTTPTimeout = 10 * time.Second + LongHTTPTimeout = 45 * time.Second ) // LFGroup contains access information of lf LDAP group type LFGroup struct { - LfBaseURL string - ClientID string - ClientSecret string - RefreshToken string + LfBaseURL string + ClientID string + ClientSecret string + RefreshToken string + EventsService events.Service } // LDAPGroup model @@ -30,18 +42,24 @@ type LDAPGroup struct { Title string `json:"title"` } -func (lfg *LFGroup) getAccessToken() (string, error) { +func (lfg *LFGroup) getAccessToken(ctx context.Context) (string, error) { + f := logrus.Fields{ + "functionName": "v1.gerrits.lf_group.getAccessToken", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } requestBody, err := json.Marshal(map[string]string{ "grant_type": "refresh_token", "refresh_token": lfg.RefreshToken, "scope": "manage_groups", }) if err != nil { + log.WithFields(f).WithError(err).Warn("problem encoding access token request") return "", err } OauthURL := fmt.Sprintf("%s/oauth2/token", lfg.LfBaseURL) req, err := http.NewRequest("POST", OauthURL, bytes.NewBuffer(requestBody)) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating a new request to URL: %s", OauthURL) return "", err } req.SetBasicAuth(lfg.ClientID, lfg.ClientSecret) @@ -52,32 +70,53 @@ func (lfg *LFGroup) getAccessToken() (string, error) { } res, err := client.Do(req) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem sending a request to URL: %s", OauthURL) return "", err } - defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + + defer func() { + closeErr := res.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing response body") + } + }() + + body, err := io.ReadAll(res.Body) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response from URL: %s", OauthURL) return "", err } + var out struct { AccessToken string `json:"access_token"` } + err = json.Unmarshal(body, &out) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response from URL: %s", OauthURL) return "", err } + return out.AccessToken, nil } // GetGroup returns LF LDAP group -func (lfg *LFGroup) GetGroup(groupID string) (*LDAPGroup, error) { - accessToken, err := lfg.getAccessToken() +func (lfg *LFGroup) GetGroup(ctx context.Context, groupID string) (*LDAPGroup, error) { + f := logrus.Fields{ + "functionName": "v1.gerrits.lf_group.GetGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "groupID": groupID, + } + + accessToken, err := lfg.getAccessToken(ctx) if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading access token") return nil, err } getGroupURL := fmt.Sprintf("%s/rest/auth0/og/%s", lfg.LfBaseURL, groupID) req, err := http.NewRequest("GET", getGroupURL, nil) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating a new request to URL: %s", getGroupURL) return nil, err } req.Header.Add("Content-Type", "application/json") @@ -88,17 +127,274 @@ func (lfg *LFGroup) GetGroup(groupID string) (*LDAPGroup, error) { } res, err := client.Do(req) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem invoking request to URL: %s", getGroupURL) return nil, err } - defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + + defer func() { + closeErr := res.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing response body") + } + }() + + body, err := io.ReadAll(res.Body) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response from URL: %s", getGroupURL) return nil, err } + var out LDAPGroup err = json.Unmarshal(body, &out) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response from URL: %s", getGroupURL) return nil, err } + return &out, nil } + +// GetUsersOfGroup returns a list of members from a group +func (lfg *LFGroup) GetUsersOfGroup(ctx context.Context, authUser *auth.User, claGroupID, groupName string) (*v2Models.GerritGroupResponse, error) { + f := logrus.Fields{ + "functionName": "v1.gerrits.lf_group.GetUsersOfGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "groupName": groupName, + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, + } + + log.WithFields(f).Debug("getting users of group...") + + // Fetch a token for authorization + accessToken, err := lfg.getAccessToken(ctx) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading access token") + return nil, err + } + + // Build the URL path - can take the groupName or numeric value + // API Docs: https://confluence.linuxfoundation.org/display/IPM/Drupal+Identity+REST+for+Auth0 + url := fmt.Sprintf("%s/rest/auth0/og/%s", lfg.LfBaseURL, groupName) + + // Set up the request + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating a new request to URL: %s", url) + return nil, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+accessToken) + client := http.Client{ + Timeout: LongHTTPTimeout, + } + + // Invoke the request + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem invoking request to URL: %s", url) + return nil, err + } + + // Cleanup after + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing response body") + } + }() + + // Check the response code to see how it went - response payload is undefined - just looking for a successful response status code + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + log.WithFields(f).Debugf("successfully fetched members from group: %s", groupName) + + var result v2Models.GerritGroupResponse + body, err := io.ReadAll(resp.Body) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading response for url: %s", url) + return nil, err + } + + log.WithFields(f).Debugf("response body: %+v", string(body)) + err = json.Unmarshal(body, &result) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling response for url: %s", url) + return nil, err + } + + return &result, nil + } + + log.WithFields(f).Warnf("error fetching users from group: %s - response status: %d", groupName, resp.StatusCode) + return nil, nil +} + +// AddUserToGroup adds the specified user to the group +func (lfg *LFGroup) AddUserToGroup(ctx context.Context, authUser *auth.User, claGroupID, groupName, userName string) error { + f := logrus.Fields{ + "functionName": "v1.gerrits.lf_group.AddUserToGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "groupName": groupName, + "userName": userName, + } + + log.WithFields(f).Debug("adding user to group...") + + // Fetch a token for authorization + accessToken, err := lfg.getAccessToken(ctx) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading access token") + return err + } + + // Build the URL path - can take the groupName or numeric value + // API Docs: https://confluence.linuxfoundation.org/display/IPM/Drupal+Identity+REST+for+Auth0 + url := fmt.Sprintf("%s/rest/auth0/og/%s", lfg.LfBaseURL, groupName) + + // Build the request payload + payload := map[string]interface{}{ + "username": userName, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + log.WithFields(f).Warnf("unable to encode payload for the request to URL: %s", url) + return err + } + + // Set up the request + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating a new request to URL: %s", url) + return err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+accessToken) + client := http.Client{ + Timeout: DefaultHTTPTimeout, + } + + // Invoke the request + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem invoking request to URL: %s", url) + return err + } + + // Cleanup after + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing response body") + } + }() + + // Check the response code to see how it went - response payload is undefined - just looking for a successful response status code + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + log.WithFields(f).Debugf("successfully added user: %s to group: %s", userName, groupName) + lfUsername := "" + username := "" + if authUser != nil { + lfUsername = authUser.UserName + username = authUser.UserName + } + // Create a log event indicating our success + lfg.EventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.GerritUserAdded, + LfUsername: lfUsername, + UserName: username, + CLAGroupID: claGroupID, + EventData: &events.GerritUserAddedEventData{ + Username: userName, + GroupName: groupName, + }, + }) + } else { + log.WithFields(f).Warnf("error adding added user: %s to group: %s - response status: %d", userName, groupName, resp.StatusCode) + } + + return nil +} + +// RemoveUserFromGroup removes the specified user from the group +func (lfg *LFGroup) RemoveUserFromGroup(ctx context.Context, authUser *auth.User, claGroupID, groupName, userName string) error { + f := logrus.Fields{ + "functionName": "v1.gerrits.lf_group.RemoveUserFromGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "groupName": groupName, + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, + } + + log.WithFields(f).Debug("removing user from group...") + + // Fetch a token for authorization + accessToken, err := lfg.getAccessToken(ctx) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading access token") + return err + } + + // Build the URL path - can take the groupName or numeric value + // API Docs: https://confluence.linuxfoundation.org/display/IPM/Drupal+Identity+REST+for+Auth0 + url := fmt.Sprintf("%s/rest/auth0/og/%s", lfg.LfBaseURL, groupName) + + // Build the request payload + payload := map[string]interface{}{ + "username": userName, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + log.WithFields(f).Warnf("unable to encode payload for the request to URL: %s", url) + return err + } + + // Set up the request + req, err := http.NewRequest("DELETE", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating a new request to URL: %s", url) + return err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+accessToken) + client := http.Client{ + Timeout: DefaultHTTPTimeout, + } + + // Invoke the request + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem invoking request to URL: %s", url) + return err + } + + // Cleanup after + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("error closing response body") + } + }() + + // Check the response code to see how it went - response payload is undefined - just looking for a successful response status code + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + log.WithFields(f).Debugf("successfully removed user: %s from group: %s", userName, groupName) + // Create a log event indicating our success + lfg.EventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.GerritUserRemoved, + LfUsername: authUser.UserName, + UserName: authUser.UserName, + CLAGroupID: claGroupID, + EventData: &events.GerritUserRemovedEventData{ + Username: userName, + GroupName: groupName, + }, + }) + } else { + log.WithFields(f).Warnf("error removing user: %s from group: %s - response status: %d", userName, groupName, resp.StatusCode) + } + + return nil +} diff --git a/cla-backend-go/gerrits/mocks/mock_repository.go b/cla-backend-go/gerrits/mocks/mock_repository.go new file mode 100644 index 000000000..6d45afc36 --- /dev/null +++ b/cla-backend-go/gerrits/mocks/mock_repository.go @@ -0,0 +1,143 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: gerrits/repository.go + +// Package mock_gerrits is a generated GoMock package. +package mock_gerrits + +import ( + context "context" + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// AddGerrit mocks base method. +func (m *MockRepository) AddGerrit(ctx context.Context, input *models.Gerrit) (*models.Gerrit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddGerrit", ctx, input) + ret0, _ := ret[0].(*models.Gerrit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddGerrit indicates an expected call of AddGerrit. +func (mr *MockRepositoryMockRecorder) AddGerrit(ctx, input interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGerrit", reflect.TypeOf((*MockRepository)(nil).AddGerrit), ctx, input) +} + +// DeleteGerrit mocks base method. +func (m *MockRepository) DeleteGerrit(ctx context.Context, gerritID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteGerrit", ctx, gerritID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteGerrit indicates an expected call of DeleteGerrit. +func (mr *MockRepositoryMockRecorder) DeleteGerrit(ctx, gerritID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGerrit", reflect.TypeOf((*MockRepository)(nil).DeleteGerrit), ctx, gerritID) +} + +// ExistsByName mocks base method. +func (m *MockRepository) ExistsByName(ctx context.Context, gerritName string) ([]*models.Gerrit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExistsByName", ctx, gerritName) + ret0, _ := ret[0].([]*models.Gerrit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExistsByName indicates an expected call of ExistsByName. +func (mr *MockRepositoryMockRecorder) ExistsByName(ctx, gerritName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsByName", reflect.TypeOf((*MockRepository)(nil).ExistsByName), ctx, gerritName) +} + +// GetClaGroupGerrits mocks base method. +func (m *MockRepository) GetClaGroupGerrits(ctx context.Context, claGroupID string) (*models.GerritList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupGerrits", ctx, claGroupID) + ret0, _ := ret[0].(*models.GerritList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupGerrits indicates an expected call of GetClaGroupGerrits. +func (mr *MockRepositoryMockRecorder) GetClaGroupGerrits(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupGerrits", reflect.TypeOf((*MockRepository)(nil).GetClaGroupGerrits), ctx, claGroupID) +} + +// GetGerrit mocks base method. +func (m *MockRepository) GetGerrit(ctx context.Context, gerritID string) (*models.Gerrit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGerrit", ctx, gerritID) + ret0, _ := ret[0].(*models.Gerrit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGerrit indicates an expected call of GetGerrit. +func (mr *MockRepositoryMockRecorder) GetGerrit(ctx, gerritID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGerrit", reflect.TypeOf((*MockRepository)(nil).GetGerrit), ctx, gerritID) +} + +// GetGerritsByID mocks base method. +func (m *MockRepository) GetGerritsByID(ctx context.Context, ID, IDType string) (*models.GerritList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGerritsByID", ctx, ID, IDType) + ret0, _ := ret[0].(*models.GerritList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGerritsByID indicates an expected call of GetGerritsByID. +func (mr *MockRepositoryMockRecorder) GetGerritsByID(ctx, ID, IDType interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGerritsByID", reflect.TypeOf((*MockRepository)(nil).GetGerritsByID), ctx, ID, IDType) +} + +// GetGerritsByProjectSFID mocks base method. +func (m *MockRepository) GetGerritsByProjectSFID(ctx context.Context, projectSFID string) (*models.GerritList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGerritsByProjectSFID", ctx, projectSFID) + ret0, _ := ret[0].(*models.GerritList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGerritsByProjectSFID indicates an expected call of GetGerritsByProjectSFID. +func (mr *MockRepositoryMockRecorder) GetGerritsByProjectSFID(ctx, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGerritsByProjectSFID", reflect.TypeOf((*MockRepository)(nil).GetGerritsByProjectSFID), ctx, projectSFID) +} diff --git a/cla-backend-go/gerrits/mocks/mock_service.go b/cla-backend-go/gerrits/mocks/mock_service.go new file mode 100644 index 000000000..64ba64d65 --- /dev/null +++ b/cla-backend-go/gerrits/mocks/mock_service.go @@ -0,0 +1,143 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: gerrits/service.go + +// Package mock_gerrits is a generated GoMock package. +package mock_gerrits + +import ( + context "context" + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + 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 +} + +// AddGerrit mocks base method. +func (m *MockService) AddGerrit(ctx context.Context, claGroupID, projectSFID string, input *models.AddGerritInput, claGroupModel *models.ClaGroup) (*models.Gerrit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddGerrit", ctx, claGroupID, projectSFID, input, claGroupModel) + ret0, _ := ret[0].(*models.Gerrit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddGerrit indicates an expected call of AddGerrit. +func (mr *MockServiceMockRecorder) AddGerrit(ctx, claGroupID, projectSFID, input, claGroupModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGerrit", reflect.TypeOf((*MockService)(nil).AddGerrit), ctx, claGroupID, projectSFID, input, claGroupModel) +} + +// DeleteClaGroupGerrits mocks base method. +func (m *MockService) DeleteClaGroupGerrits(ctx context.Context, claGroupID string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteClaGroupGerrits", ctx, claGroupID) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteClaGroupGerrits indicates an expected call of DeleteClaGroupGerrits. +func (mr *MockServiceMockRecorder) DeleteClaGroupGerrits(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteClaGroupGerrits", reflect.TypeOf((*MockService)(nil).DeleteClaGroupGerrits), ctx, claGroupID) +} + +// DeleteGerrit mocks base method. +func (m *MockService) DeleteGerrit(ctx context.Context, gerritID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteGerrit", ctx, gerritID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteGerrit indicates an expected call of DeleteGerrit. +func (mr *MockServiceMockRecorder) DeleteGerrit(ctx, gerritID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGerrit", reflect.TypeOf((*MockService)(nil).DeleteGerrit), ctx, gerritID) +} + +// GetClaGroupGerrits mocks base method. +func (m *MockService) GetClaGroupGerrits(ctx context.Context, claGroupID string) (*models.GerritList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupGerrits", ctx, claGroupID) + ret0, _ := ret[0].(*models.GerritList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupGerrits indicates an expected call of GetClaGroupGerrits. +func (mr *MockServiceMockRecorder) GetClaGroupGerrits(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupGerrits", reflect.TypeOf((*MockService)(nil).GetClaGroupGerrits), ctx, claGroupID) +} + +// GetGerrit mocks base method. +func (m *MockService) GetGerrit(ctx context.Context, gerritID string) (*models.Gerrit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGerrit", ctx, gerritID) + ret0, _ := ret[0].(*models.Gerrit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGerrit indicates an expected call of GetGerrit. +func (mr *MockServiceMockRecorder) GetGerrit(ctx, gerritID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGerrit", reflect.TypeOf((*MockService)(nil).GetGerrit), ctx, gerritID) +} + +// GetGerritRepos mocks base method. +func (m *MockService) GetGerritRepos(ctx context.Context, gerritName string) (*models.GerritRepoList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGerritRepos", ctx, gerritName) + ret0, _ := ret[0].(*models.GerritRepoList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGerritRepos indicates an expected call of GetGerritRepos. +func (mr *MockServiceMockRecorder) GetGerritRepos(ctx, gerritName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGerritRepos", reflect.TypeOf((*MockService)(nil).GetGerritRepos), ctx, gerritName) +} + +// GetGerritsByProjectSFID mocks base method. +func (m *MockService) GetGerritsByProjectSFID(ctx context.Context, projectSFID string) (*models.GerritList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGerritsByProjectSFID", ctx, projectSFID) + ret0, _ := ret[0].(*models.GerritList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGerritsByProjectSFID indicates an expected call of GetGerritsByProjectSFID. +func (mr *MockServiceMockRecorder) GetGerritsByProjectSFID(ctx, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGerritsByProjectSFID", reflect.TypeOf((*MockService)(nil).GetGerritsByProjectSFID), ctx, projectSFID) +} diff --git a/cla-backend-go/gerrits/models.go b/cla-backend-go/gerrits/models.go index 82b309aa1..f15a074d0 100644 --- a/cla-backend-go/gerrits/models.go +++ b/cla-backend-go/gerrits/models.go @@ -4,7 +4,7 @@ package gerrits import ( - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" "github.com/go-openapi/strfmt" ) @@ -27,18 +27,15 @@ type Gerrit struct { // toModel converts the gerrit structure into a response model func (g *Gerrit) toModel() *models.Gerrit { return &models.Gerrit{ - DateCreated: g.DateCreated, - DateModified: g.DateModified, - GerritID: strfmt.UUID4(g.GerritID), - GerritName: g.GerritName, - GerritURL: strfmt.URI(g.GerritURL), - GroupIDCcla: g.GroupIDCcla, - GroupIDIcla: g.GroupIDIcla, - GroupNameCcla: g.GroupNameCcla, - GroupNameIcla: g.GroupNameIcla, - ProjectID: g.ProjectID, - Version: g.Version, - ProjectSFID: g.ProjectSFID, + DateCreated: g.DateCreated, + DateModified: g.DateModified, + GerritID: strfmt.UUID4(g.GerritID), + GerritName: g.GerritName, + GerritURL: strfmt.URI(g.GerritURL), + GroupIDCcla: g.GroupIDCcla, + ProjectID: g.ProjectID, + Version: g.Version, + ProjectSFID: g.ProjectSFID, } } diff --git a/cla-backend-go/gerrits/repository.go b/cla-backend-go/gerrits/repository.go index d93a21e5b..80207f3e3 100644 --- a/cla-backend-go/gerrits/repository.go +++ b/cla-backend-go/gerrits/repository.go @@ -23,22 +23,23 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" - "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" ) // errors var ( ErrGerritNotFound = errors.New("gerrit not found") + HugePageSize = int64(10000) ) -// Repository defines functions of Repositories +// Repository defines functions of V3Repositories type Repository interface { AddGerrit(ctx context.Context, input *models.Gerrit) (*models.Gerrit, error) GetGerrit(ctx context.Context, gerritID string) (*models.Gerrit, error) GetGerritsByID(ctx context.Context, ID string, IDType string) (*models.GerritList, error) GetGerritsByProjectSFID(ctx context.Context, projectSFID string) (*models.GerritList, error) - GetClaGroupGerrits(ctx context.Context, projectID string, projectSFID *string) (*models.GerritList, error) + GetClaGroupGerrits(ctx context.Context, claGroupID string) (*models.GerritList, error) ExistsByName(ctx context.Context, gerritName string) ([]*models.Gerrit, error) DeleteGerrit(ctx context.Context, gerritID string) error } @@ -61,7 +62,7 @@ type repo struct { // AddGerrit creates a new gerrit instance func (repo *repo) AddGerrit(ctx context.Context, input *models.Gerrit) (*models.Gerrit, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.AddGerrit", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } gerritID, err := uuid.NewV4() @@ -70,18 +71,15 @@ func (repo *repo) AddGerrit(ctx context.Context, input *models.Gerrit) (*models. } _, currentTime := utils.CurrentTime() gerrit := &Gerrit{ - DateCreated: currentTime, - DateModified: currentTime, - GerritID: gerritID.String(), - GerritName: input.GerritName, - GerritURL: input.GerritURL.String(), - GroupIDCcla: input.GroupIDCcla, - GroupIDIcla: input.GroupIDIcla, - GroupNameCcla: input.GroupNameCcla, - GroupNameIcla: input.GroupNameIcla, - ProjectID: input.ProjectID, - ProjectSFID: input.ProjectSFID, - Version: input.Version, + DateCreated: currentTime, + DateModified: currentTime, + GerritID: gerritID.String(), + GerritName: input.GerritName, + GerritURL: input.GerritURL.String(), + GroupIDCcla: input.GroupIDCcla, + ProjectID: input.ProjectID, + ProjectSFID: input.ProjectSFID, + Version: input.Version, } av, err := dynamodbattribute.MarshalMap(gerrit) if err != nil { @@ -103,7 +101,7 @@ func (repo *repo) AddGerrit(ctx context.Context, input *models.Gerrit) (*models. // GetGerrit returns the gerrit instances based on the ID func (repo *repo) GetGerrit(ctx context.Context, gerritID string) (*models.Gerrit, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.GetGerrit", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritID": gerritID, } @@ -137,7 +135,7 @@ func (repo *repo) GetGerrit(ctx context.Context, gerritID string) (*models.Gerri func (repo repo) GetGerritsByID(ctx context.Context, ID string, IDType string) (*models.GerritList, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.GetGerritsByID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "ID": ID, "IDType": IDType, @@ -200,7 +198,7 @@ func (repo repo) GetGerritsByID(ctx context.Context, ID string, IDType string) ( func (repo repo) GetGerritsByProjectSFID(ctx context.Context, projectSFID string) (*models.GerritList, error) { f := logrus.Fields{ - "functionName": "GetGerritsByProjectSFID", + "functionName": "v1.gerrits.repository.GetGerritsByProjectSFID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, } @@ -258,35 +256,37 @@ func (repo repo) GetGerritsByProjectSFID(ctx context.Context, projectSFID string return &models.GerritList{List: resultList}, nil } -// GetClaGroupGerrits returns the CLA Group gerrit instances based on the CLA Group ID and the project SFID -func (repo repo) GetClaGroupGerrits(ctx context.Context, projectID string, projectSFID *string) (*models.GerritList, error) { +// GetClaGroupGerrits returns the CLA Group gerrit instances based on the CLA Group ID +func (repo repo) GetClaGroupGerrits(ctx context.Context, claGroupID string) (*models.GerritList, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.GetClaGroupGerrits", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectID": projectID, - "projectSFID": projectSFID, + "claGroupID": claGroupID, } resultList := make([]*models.Gerrit, 0) - filter := expression.Name("project_id").Equal(expression.Value(projectID)) - if projectSFID != nil { - filter = filter.And(expression.Name("project_sfid").Equal(expression.Value(*projectSFID))) - } - expr, err := expression.NewBuilder().WithFilter(filter).Build() + condition := expression.Key("project_id").Equal(expression.Value(claGroupID)) + + expr, err := expression.NewBuilder().WithKeyCondition(condition).Build() if err != nil { - log.WithFields(f).Warnf("error building expression for gerrit instances scan, error: %v", err) + log.WithFields(f).Warnf("error building expression for gerrit instances query, error: %v", err) return nil, err } + // Assemble the query input parameters - scanInput := &dynamodb.ScanInput{ + queryInput := &dynamodb.QueryInput{ ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), TableName: aws.String(repo.tableName), + IndexName: aws.String("gerrit-project-id-index"), + Limit: aws.Int64(HugePageSize), } for { - results, err := repo.dynamoDBClient.Scan(scanInput) + results, err := repo.dynamoDBClient.Query(queryInput) if err != nil { log.WithFields(f).WithError(err).Warnf("error retrieving gerrit instances, error: %v", err) return nil, err @@ -305,7 +305,7 @@ func (repo repo) GetClaGroupGerrits(ctx context.Context, projectID string, proje } if len(results.LastEvaluatedKey) != 0 { - scanInput.ExclusiveStartKey = results.LastEvaluatedKey + queryInput.ExclusiveStartKey = results.LastEvaluatedKey } else { break } @@ -322,7 +322,7 @@ func (repo repo) GetClaGroupGerrits(ctx context.Context, projectID string, proje // DeleteGerrit removes the gerrit instance based on the gerrit ID func (repo *repo) DeleteGerrit(ctx context.Context, gerritID string) error { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.DeleteGerrit", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritID": gerritID, } @@ -347,7 +347,7 @@ func (repo *repo) DeleteGerrit(ctx context.Context, gerritID string) error { func (repo *repo) ExistsByName(ctx context.Context, gerritName string) ([]*models.Gerrit, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.ExistsByName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritName": gerritName, } @@ -419,7 +419,7 @@ func (repo *repo) ExistsByName(ctx context.Context, gerritName string) ([]*model func (repo *repo) ExistsByID(ctx context.Context, gerritID string) ([]*models.Gerrit, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.repository.ExistsByID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritID": gerritID, } diff --git a/cla-backend-go/gerrits/service.go b/cla-backend-go/gerrits/service.go index 180f1fbbf..0fb218cce 100644 --- a/cla-backend-go/gerrits/service.go +++ b/cla-backend-go/gerrits/service.go @@ -8,17 +8,25 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "net/url" "strings" + "time" - "github.com/go-openapi/strfmt" + // "github.com/LF-Engineering/lfx-kit/auth" + "github.com/go-openapi/strfmt" "github.com/go-resty/resty/v2" + + // "github.com/go-resty/resty/v2" + apiclient "github.com/communitybridge/easycla/cla-backend-go/api_client" "github.com/sirupsen/logrus" "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" + // v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" ) @@ -28,64 +36,36 @@ type Service interface { AddGerrit(ctx context.Context, claGroupID string, projectSFID string, input *models.AddGerritInput, claGroupModel *models.ClaGroup) (*models.Gerrit, error) GetGerrit(ctx context.Context, gerritID string) (*models.Gerrit, error) GetGerritsByProjectSFID(ctx context.Context, projectSFID string) (*models.GerritList, error) - GetClaGroupGerrits(ctx context.Context, claGroupID string, projectSFID *string) (*models.GerritList, error) + GetClaGroupGerrits(ctx context.Context, claGroupID string) (*models.GerritList, error) GetGerritRepos(ctx context.Context, gerritName string) (*models.GerritRepoList, error) DeleteClaGroupGerrits(ctx context.Context, claGroupID string) (int, error) DeleteGerrit(ctx context.Context, gerritID string) error } type service struct { - repo Repository - lfGroup *LFGroup + repo Repository } // NewService creates a new gerrit service -func NewService(repo Repository, lfg *LFGroup) Service { +func NewService(repo Repository) Service { return service{ - repo: repo, - lfGroup: lfg, + repo: repo, } } func (s service) AddGerrit(ctx context.Context, claGroupID string, projectSFID string, params *models.AddGerritInput, claGroupModel *models.ClaGroup) (*models.Gerrit, error) { f := logrus.Fields{ - "functionName": "gerrits.AddGerrit", + "functionName": "v1.gerrits.service.AddGerrit", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "projectSFID": projectSFID, } - if params.GroupIDIcla == "" && params.GroupIDCcla == "" { - return nil, errors.New("should specify at least a LDAP group for ICLA or CCLA") - } - - log.WithFields(f).Debugf("cla groupID %s", claGroupID) - log.WithFields(f).Debugf("project Model %+v", claGroupModel) - - if claGroupModel.ProjectCCLAEnabled && claGroupModel.ProjectICLAEnabled { - if params.GroupIDCcla == "" { - return nil, errors.New("please provide GroupIDCcla") - } - if params.GroupIDIcla == "" { - return nil, errors.New("please provide GroupIDIcla") - } - } else if claGroupModel.ProjectCCLAEnabled { - if params.GroupIDCcla == "" { - return nil, errors.New("please provide GroupIDCcla") - } - } else if claGroupModel.ProjectICLAEnabled { - if params.GroupIDIcla == "" { - return nil, errors.New("please provide GroupIDIcla") - } - } - - if params.GroupIDIcla == params.GroupIDCcla { - return nil, errors.New("LDAP group for ICLA and CCLA are same") - } if params.GerritName == nil { return nil, errors.New("gerrit_name required") } + log.WithFields(f).Debugf("checking if gerrit name already exists in the system : %s", *params.GerritName) gerritObject, err := s.repo.ExistsByName(ctx, *params.GerritName) if err != nil { message := fmt.Sprintf("unable to get gerrit by name : %s", *params.GerritName) @@ -96,61 +76,46 @@ func (s service) AddGerrit(ctx context.Context, claGroupID string, projectSFID s return nil, errors.New("gerrit_name already present in the system") } - gerritCcla, err := s.repo.GetGerritsByID(ctx, params.GroupIDCcla, "CCLA") - if err != nil { - message := fmt.Sprintf("unable to get gerrit by ccla id : %s", params.GroupIDCcla) - log.WithFields(f).WithError(err).Warnf(message) + if params.GerritURL == nil { + return nil, errors.New("gerrit_url required") } - if len(gerritCcla.List) > 0 { - return nil, errors.New("gerrit_ccla id already present in the system") + input := &models.Gerrit{ + GerritName: utils.StringValue(params.GerritName), + GerritURL: strfmt.URI(*params.GerritURL), + ProjectID: claGroupID, + ProjectSFID: projectSFID, + Version: params.Version, } - gerritIcla, err := s.repo.GetGerritsByID(ctx, params.GroupIDIcla, "ICLA") + // Get the gerrit repos + log.WithFields(f).Debugf("fetching gerrit repos for gerrit instance: %s", *params.GerritURL) + gerritHost, err := extractGerritHost(*params.GerritURL, f) if err != nil { - message := fmt.Sprintf("unable to get gerrit by icla : %s", params.GroupIDIcla) - log.WithFields(f).WithError(err).Warnf(message) - } - - if len(gerritIcla.List) > 0 { - return nil, errors.New("gerrit_icla id already present in the system") + return nil, err } - - if params.GerritURL == nil { - return nil, errors.New("gerrit_url required") + gerritRepoList, getRepoErr := s.GetGerritRepos(ctx, gerritHost) + if getRepoErr != nil { + log.WithFields(f).WithError(getRepoErr).Warnf("problem fetching gerrit repos, error: %+v", getRepoErr) + return nil, getRepoErr } - var groupNameCcla, groupNameIcla string - if params.GroupIDIcla != "" { - group, err := s.lfGroup.GetGroup(params.GroupIDIcla) - if err != nil { - message := fmt.Sprintf("unable to get LDAP ICLA Group: %s", params.GroupIDIcla) - log.WithFields(f).WithError(err).Warnf(message) - return nil, errors.New(message) - } - groupNameIcla = group.Title + log.WithFields(f).Debugf("discovered %d gerrit repos", len(gerritRepoList.Repos)) + log.WithFields(f).Debugf("gerrit repo list %+v", gerritRepoList) + // Set the connected flag - for now, we just set this value to true + for _, repo := range gerritRepoList.Repos { + repo.Connected = true } - if params.GroupIDCcla != "" { - group, err := s.lfGroup.GetGroup(params.GroupIDCcla) - if err != nil { - message := fmt.Sprintf("unable to get LDAP CCLA Group: %s", params.GroupIDCcla) - log.WithFields(f).WithError(err).Warnf(message) - return nil, errors.New(message) - } - groupNameCcla = group.Title + gerritInstance, err := s.repo.AddGerrit(ctx, input) + if err != nil { + return nil, err } - input := &models.Gerrit{ - GerritName: utils.StringValue(params.GerritName), - GerritURL: strfmt.URI(*params.GerritURL), - GroupIDCcla: params.GroupIDCcla, - GroupIDIcla: params.GroupIDIcla, - GroupNameCcla: groupNameCcla, - GroupNameIcla: groupNameIcla, - ProjectID: claGroupID, - ProjectSFID: projectSFID, - Version: params.Version, - } - return s.repo.AddGerrit(ctx, input) + input.GerritID = gerritInstance.GerritID + input.DateCreated = gerritInstance.DateCreated + input.DateModified = gerritInstance.DateModified + input.GerritRepoList = gerritRepoList + log.WithFields(f).Debugf("gerrit input %+v", input) + return input, nil } func (s service) GetGerrit(ctx context.Context, gerritID string) (*models.Gerrit, error) { @@ -162,19 +127,19 @@ func (s service) GetGerritsByProjectSFID(ctx context.Context, projectSFID string return s.repo.GetGerritsByProjectSFID(ctx, projectSFID) } -func (s service) GetClaGroupGerrits(ctx context.Context, claGroupID string, projectSFID *string) (*models.GerritList, error) { +func (s service) GetClaGroupGerrits(ctx context.Context, claGroupID string) (*models.GerritList, error) { f := logrus.Fields{ - "functionName": "gerrits.GetClaGroupGerrits", + "functionName": "v1.gerrits.service.GetClaGroupGerrits", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, - "projectSFID": *projectSFID, } - responseModel, err := s.repo.GetClaGroupGerrits(ctx, claGroupID, projectSFID) + responseModel, err := s.repo.GetClaGroupGerrits(ctx, claGroupID) if err != nil { log.WithFields(f).Warnf("problem getting CLA Group gerrits, error: %+v", err) return nil, err } + log.WithFields(f).Debugf("discovered %d gerrits", len(responseModel.List)) // Add the repo list to the response model for _, gerrit := range responseModel.List { log.WithFields(f).Debugf("Processing gerrit URL: %s", gerrit.GerritURL) @@ -221,7 +186,7 @@ func extractGerritHost(gerritHost string, f logrus.Fields) (string, error) { func (s service) GetGerritRepos(ctx context.Context, gerritHost string) (*models.GerritRepoList, error) { f := logrus.Fields{ - "functionName": "gerrits.GetGerritRepos", + "functionName": "v1.gerrits.service.GetGerritRepos", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritName": gerritHost, } @@ -242,12 +207,19 @@ func (s service) GetGerritRepos(ctx context.Context, gerritHost string) (*models } func (s service) DeleteClaGroupGerrits(ctx context.Context, claGroupID string) (int, error) { - gerrits, err := s.repo.GetClaGroupGerrits(ctx, claGroupID, nil) + f := logrus.Fields{ + "functionName": "v1.gerrits.service.DeleteClaGroupGerrits", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + } + + gerrits, err := s.repo.GetClaGroupGerrits(ctx, claGroupID) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem fetching gerrits for CLA Group: %s", claGroupID) return 0, err } if len(gerrits.List) > 0 { - log.Debugf(fmt.Sprintf("Deleting gerrits for cla-group :%s ", claGroupID)) + log.WithFields(f).Debugf(fmt.Sprintf("Deleting gerrits for cla-group :%s ", claGroupID)) for _, gerrit := range gerrits.List { err = s.repo.DeleteGerrit(ctx, gerrit.GerritID.String()) if err != nil { @@ -255,6 +227,7 @@ func (s service) DeleteClaGroupGerrits(ctx context.Context, claGroupID string) ( } } } + return len(gerrits.List), nil } @@ -274,6 +247,7 @@ func convertModel(responseModel map[string]GerritRepoInfo, serverInfo *ServerInf URL: strfmt.URI(weblink.URL), }) } + log.Debugf("Processing repo: %s, weblinks: %+v", name, weblinks) claEnabled := false if serverInfo != nil && serverInfo.Auth.UseContributorAgreements { @@ -314,35 +288,57 @@ func buildContributorAgreementDetails(serverInfo *ServerInfo) []*models.GerritRe // listGerritRepos returns a list of gerrit repositories for the given gerrit host func listGerritRepos(ctx context.Context, gerritHost string) (map[string]GerritRepoInfo, error) { f := logrus.Fields{ - "functionName": "gerrits.listGerritRepos", + "functionName": "v1.gerrits.listGerritRepos", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritHost": gerritHost, } - client := resty.New() + client := &apiclient.RestAPIClient{ + Client: &http.Client{ + Timeout: 10 * time.Second, + }, + } + + base := "https://" + gerritHost gerritAPIPath, gerritAPIPathErr := getGerritAPIPath(ctx, gerritHost) if gerritAPIPathErr != nil { return nil, gerritAPIPathErr } - resp, err := client.R(). - EnableTrace(). - Get(fmt.Sprintf("https://%s/%s/projects/?d&pp=0", gerritHost, gerritAPIPath)) + log.WithFields(f).Debugf("gerrit API path using client: %s", gerritAPIPath) + + if gerritAPIPath != "" { + base = fmt.Sprintf("https://%s/%s", gerritHost, gerritAPIPath) + } + + url := fmt.Sprintf("%s/projects/?d&pp=0", base) + resp, err := client.GetData(ctx, url) + if err != nil { - log.WithFields(f).Warnf("problem querying gerrit host: %s, error: %+v", gerritHost, err) return nil, err } - if resp.IsError() { - msg := fmt.Sprintf("non-success response from list gerrit host repos for gerrit %s, error code: %s", gerritHost, resp.Status()) - log.WithFields(f).Warn(msg) - return nil, errors.New(msg) + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).Debugf("Failed to close response body; %v", err) + } + }() + + log.WithFields(f).Debugf("response: %+v", resp.Body) + + // Get the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err } - var result map[string]GerritRepoInfo // Need to strip off the leading "magic prefix line" from the response payload, which is: )]}' // See: https://gerrit.linuxfoundation.org/infra/Documentation/rest-api.html#output - err = json.Unmarshal(resp.Body()[4:], &result) + strippedBody := stripMagicPrefix(body) + + var result map[string]GerritRepoInfo + + err = json.Unmarshal(strippedBody, &result) if err != nil { log.WithFields(f).Warnf("problem unmarshalling response for gerrit host: %s, error: %+v", gerritHost, err) return nil, err @@ -351,23 +347,36 @@ func listGerritRepos(ctx context.Context, gerritHost string) (map[string]GerritR return result, nil } +func stripMagicPrefix(data []byte) []byte { + if len(data) > 4 { + return data[4:] + } + return data +} + // getGerritConfig returns the gerrit configuration for the specified host func getGerritConfig(ctx context.Context, gerritHost string) (*ServerInfo, error) { f := logrus.Fields{ - "functionName": "gerrits.getGerritConfig", + "functionName": "v1.gerrits.getGerritConfig", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritHost": gerritHost, } client := resty.New() + base := "https://" + gerritHost + gerritAPIPath, gerritAPIPathErr := getGerritAPIPath(ctx, gerritHost) if gerritAPIPathErr != nil { return nil, gerritAPIPathErr } + if gerritAPIPath != "" { + base = fmt.Sprintf("https://%s/%s", gerritHost, gerritAPIPath) + } + resp, err := client.R(). EnableTrace(). - Get(fmt.Sprintf("https://%s/%s/config/server/info", gerritHost, gerritAPIPath)) + Get(fmt.Sprintf("%s/config/server/info", base)) if err != nil { log.WithFields(f).Warnf("problem querying gerrit config, error: %+v", err) return nil, err @@ -394,13 +403,15 @@ func getGerritConfig(ctx context.Context, gerritHost string) (*ServerInfo, error // getGerritAPIPath returns the path to the API based on the gerrit host func getGerritAPIPath(ctx context.Context, gerritHost string) (string, error) { f := logrus.Fields{ - "functionName": "gerrits.getGerritAPIPath", + "functionName": "v1.gerrits.getGerritAPIPath", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gerritHost": gerritHost, } switch gerritHost { case "gerrit.linuxfoundation.org": return "infra", nil + case "mockapi.gerrit.dev.itx.linuxfoundation.org": + return "", nil case "gerrit.onap.org": return "r", nil case "gerrit.o-ran-sc.org": diff --git a/cla-backend-go/gerrits/service_test.go b/cla-backend-go/gerrits/service_test.go new file mode 100644 index 000000000..3374211bf --- /dev/null +++ b/cla-backend-go/gerrits/service_test.go @@ -0,0 +1,123 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gerrits + +import ( + // "bytes" + "context" + // "io" + // "net/http" + "testing" + + // mock_apiclient "github.com/communitybridge/easycla/cla-backend-go/api_client/mocks" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + gerritsMock "github.com/communitybridge/easycla/cla-backend-go/gerrits/mocks" + "github.com/go-openapi/strfmt" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestService_AddGerrit(t *testing.T) { + + gerritName := "gerritName" + gerritURL := "https://mockapi.gerrit.dev.itx.linuxfoundation.org/" + // gerritHost := "mockapi.gerrit.dev.itx.linuxfoundation.org" + repos := []*models.GerritRepo{ + { + ClaEnabled: true, + Connected: true, + ContributorAgreements: []*models.GerritRepoContributorAgreementsItems0{ + { + Description: "CCLA (Corporate Contributor License Agreement) for SUN", + Name: "CCLA", + URL: "https://api.dev.lfcla.com/v2/gerrit/01af041c-fa69-4052-a23c-fb8c1d3bef24/corporate/agreementUrl.html", + }, + { + Description: "ICLA (Individual Contributor License Agreement) for SUN", + Name: "ICLA", + URL: "https://api.dev.lfcla.com/v2/gerrit/01af041c-fa69-4052-a23c-fb8c1d3bef24/individual/agreementUrl.html", + }, + }, + Description: "Access inherited by all other projects.", + ID: "All-Projects", + Name: "All-Projects", + State: "ACTIVE", + WebLinks: []*models.GerritRepoWebLinksItems0{ + { + Name: "browse", + URL: "/plugins/gitiles/All-Projects", + }, + }, + }, + } + + testCases := []struct { + name string + claGroupID string + projectSFID string + params *models.AddGerritInput + gerritRepoList *models.GerritRepoList + ReposExist []*models.Gerrit + repoListErr error + claGroupModel *models.ClaGroup + expectedResult *models.Gerrit + expectedError error + }{ + { + name: "Valid input", + claGroupID: "claGroupID", + projectSFID: "projectSFID", + params: &models.AddGerritInput{ + GerritName: &gerritName, + GerritURL: &gerritURL, + Version: "version", + }, + ReposExist: []*models.Gerrit{}, + gerritRepoList: &models.GerritRepoList{ + Repos: repos, + }, + repoListErr: nil, + claGroupModel: &models.ClaGroup{}, + expectedResult: &models.Gerrit{ + GerritName: gerritName, + GerritURL: strfmt.URI(gerritURL), + ProjectID: "claGroupID", + ProjectSFID: "projectSFID", + Version: "version", + GerritRepoList: &models.GerritRepoList{ + Repos: repos, + }, + }, + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockRepo := gerritsMock.NewMockRepository(ctrl) + mockRepo.EXPECT().ExistsByName(context.Background(), gerritName).Return(tc.ReposExist, nil) + gerrit := &models.Gerrit{ + GerritName: gerritName, + GerritURL: strfmt.URI(gerritURL), + ProjectID: "claGroupID", + ProjectSFID: "projectSFID", + Version: "version", + GerritRepoList: tc.gerritRepoList, + } + + mockRepo.EXPECT().AddGerrit(gomock.Any(), gomock.Any()).Return(gerrit, nil) + + service := NewService(mockRepo) + + result, err := service.AddGerrit(context.Background(), tc.claGroupID, tc.projectSFID, tc.params, tc.claGroupModel) + + if err != nil { + t.Fatalf("Add Gerrit returned an error: %v", err) + } + assert.NotNil(t, result) + }) + } +} diff --git a/cla-backend-go/github/.graphqlconfig b/cla-backend-go/github/.graphqlconfig new file mode 100644 index 000000000..fc565542a --- /dev/null +++ b/cla-backend-go/github/.graphqlconfig @@ -0,0 +1,16 @@ +{ + "name": "GitHub API V4 GraphQL Schema", + "schemaPath": "github-schema.graphql", + "extensions": { + "endpoints": { + "GitHub API V4 GraphQL Endpoint": { + "url": "https://api.github.com/graphql", + "headers": { + "Authorization": "Bearer ${env:GITHUB-ACCESS-TOKEN}", + "user-agent": "JS GraphQL" + }, + "introspect": false + } + } + } +} diff --git a/cla-backend-go/github/branch_protection/interfaces.go b/cla-backend-go/github/branch_protection/interfaces.go new file mode 100644 index 000000000..d1f8df941 --- /dev/null +++ b/cla-backend-go/github/branch_protection/interfaces.go @@ -0,0 +1,32 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package branch_protection + +import ( + "context" + + "github.com/google/go-github/v37/github" + "github.com/shurcooL/githubv4" +) + +// V3Repositories is part of the interface working with github repositories, it's inside of the github client +// It's extracted here as interface so we can mock that functionality in the tests. +type V3Repositories interface { + ListByOrg(ctx context.Context, org string, opt *github.RepositoryListByOrgOptions) ([]*github.Repository, *github.Response, error) + Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) +} + +// V4BranchProtectionRepository has v4 (graphQL) branch protection functionality +type V4BranchProtectionRepository interface { + GetRepositoryBranchProtections(ctx context.Context, repositoryOwner, repositoryName string) (*RepoBranchProtectionQueryResult, error) + CreateBranchProtection(ctx context.Context, input *githubv4.CreateBranchProtectionRuleInput) (*CreateRepoBranchProtectionMutation, error) + UpdateBranchProtection(ctx context.Context, input *githubv4.UpdateBranchProtectionRuleInput) (*UpdateRepoBranchProtectionMutation, error) + GetRepositoryIDFromName(ctx context.Context, repositoryOwner, repositoryName string) (string, error) +} + +// CombinedRepository is combination of V3Repositories and V4BranchProtectionRepository +type CombinedRepository interface { + V3Repositories + V4BranchProtectionRepository +} diff --git a/cla-backend-go/github/branch_protection/mock.go b/cla-backend-go/github/branch_protection/mock.go new file mode 100644 index 000000000..b0ac6cd4d --- /dev/null +++ b/cla-backend-go/github/branch_protection/mock.go @@ -0,0 +1,133 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/communitybridge/easycla/cla-backend-go/github/branch_protection (interfaces: CombinedRepository) + +// Package branch_protection is a generated GoMock package. +package branch_protection + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + github "github.com/google/go-github/v37/github" + githubv4 "github.com/shurcooL/githubv4" +) + +// MockCombinedRepository is a mock of CombinedRepository interface +type MockCombinedRepository struct { + ctrl *gomock.Controller + recorder *MockCombinedRepositoryMockRecorder +} + +// MockCombinedRepositoryMockRecorder is the mock recorder for MockCombinedRepository +type MockCombinedRepositoryMockRecorder struct { + mock *MockCombinedRepository +} + +// NewMockCombinedRepository creates a new mock instance +func NewMockCombinedRepository(ctrl *gomock.Controller) *MockCombinedRepository { + mock := &MockCombinedRepository{ctrl: ctrl} + mock.recorder = &MockCombinedRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockCombinedRepository) EXPECT() *MockCombinedRepositoryMockRecorder { + return m.recorder +} + +// CreateBranchProtection mocks base method +func (m *MockCombinedRepository) CreateBranchProtection(arg0 context.Context, arg1 *githubv4.CreateBranchProtectionRuleInput) (*CreateRepoBranchProtectionMutation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateBranchProtection", arg0, arg1) + ret0, _ := ret[0].(*CreateRepoBranchProtectionMutation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateBranchProtection indicates an expected call of CreateBranchProtection +func (mr *MockCombinedRepositoryMockRecorder) CreateBranchProtection(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBranchProtection", reflect.TypeOf((*MockCombinedRepository)(nil).CreateBranchProtection), arg0, arg1) +} + +// Get mocks base method +func (m *MockCombinedRepository) Get(arg0 context.Context, arg1, arg2 string) (*github.Repository, *github.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2) + ret0, _ := ret[0].(*github.Repository) + ret1, _ := ret[1].(*github.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Get indicates an expected call of Get +func (mr *MockCombinedRepositoryMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCombinedRepository)(nil).Get), arg0, arg1, arg2) +} + +// GetRepositoryBranchProtections mocks base method +func (m *MockCombinedRepository) GetRepositoryBranchProtections(arg0 context.Context, arg1, arg2 string) (*RepoBranchProtectionQueryResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRepositoryBranchProtections", arg0, arg1, arg2) + ret0, _ := ret[0].(*RepoBranchProtectionQueryResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRepositoryBranchProtections indicates an expected call of GetRepositoryBranchProtections +func (mr *MockCombinedRepositoryMockRecorder) GetRepositoryBranchProtections(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryBranchProtections", reflect.TypeOf((*MockCombinedRepository)(nil).GetRepositoryBranchProtections), arg0, arg1, arg2) +} + +// GetRepositoryIDFromName mocks base method +func (m *MockCombinedRepository) GetRepositoryIDFromName(arg0 context.Context, arg1, arg2 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRepositoryIDFromName", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRepositoryIDFromName indicates an expected call of GetRepositoryIDFromName +func (mr *MockCombinedRepositoryMockRecorder) GetRepositoryIDFromName(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryIDFromName", reflect.TypeOf((*MockCombinedRepository)(nil).GetRepositoryIDFromName), arg0, arg1, arg2) +} + +// ListByOrg mocks base method +func (m *MockCombinedRepository) ListByOrg(arg0 context.Context, arg1 string, arg2 *github.RepositoryListByOrgOptions) ([]*github.Repository, *github.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByOrg", arg0, arg1, arg2) + ret0, _ := ret[0].([]*github.Repository) + ret1, _ := ret[1].(*github.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListByOrg indicates an expected call of ListByOrg +func (mr *MockCombinedRepositoryMockRecorder) ListByOrg(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOrg", reflect.TypeOf((*MockCombinedRepository)(nil).ListByOrg), arg0, arg1, arg2) +} + +// UpdateBranchProtection mocks base method +func (m *MockCombinedRepository) UpdateBranchProtection(arg0 context.Context, arg1 *githubv4.UpdateBranchProtectionRuleInput) (*UpdateRepoBranchProtectionMutation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateBranchProtection", arg0, arg1) + ret0, _ := ret[0].(*UpdateRepoBranchProtectionMutation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateBranchProtection indicates an expected call of UpdateBranchProtection +func (mr *MockCombinedRepositoryMockRecorder) UpdateBranchProtection(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBranchProtection", reflect.TypeOf((*MockCombinedRepository)(nil).UpdateBranchProtection), arg0, arg1) +} diff --git a/cla-backend-go/github/branch_protection/protected_branch.go b/cla-backend-go/github/branch_protection/protected_branch.go new file mode 100644 index 000000000..9e2a785e6 --- /dev/null +++ b/cla-backend-go/github/branch_protection/protected_branch.go @@ -0,0 +1,336 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package branch_protection + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/shurcooL/githubv4" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + + githubpkg "github.com/google/go-github/v37/github" +) + +const ( + // DefaultBranchName is the default branch we'll be working with if not specified + DefaultBranchName = "main" +) + +var ( + // ErrBranchNotProtected indicates the situation where the branch is not enabled for protection on github side + ErrBranchNotProtected = errors.New("not protected") +) + +type combinedRepositoryProvider struct { + V3Repositories + V4BranchProtectionRepository +} + +type branchProtectionRepositoryConfig struct { + enableBlockingLimiter bool + enableNonBlockingLimiter bool +} + +// BranchProtectionRepositoryOption enables optional parameters to BranchProtectionRepository +type BranchProtectionRepositoryOption func(config *branchProtectionRepositoryConfig) + +// EnableBlockingLimiter enables the blocking limiter +func EnableBlockingLimiter() BranchProtectionRepositoryOption { + return func(config *branchProtectionRepositoryConfig) { + config.enableBlockingLimiter = true + } +} + +// EnableNonBlockingLimiter enables the non-blocking limiter +func EnableNonBlockingLimiter() BranchProtectionRepositoryOption { + return func(config *branchProtectionRepositoryConfig) { + config.enableNonBlockingLimiter = true + } +} + +// BranchProtectionRepository contains helper methods interacting with github api related to branch protection +type BranchProtectionRepository struct { + combinedRepo CombinedRepository +} + +// NewBranchProtectionRepository creates a new BranchProtectionRepository +func NewBranchProtectionRepository(installationID int64, opts ...BranchProtectionRepositoryOption) (*BranchProtectionRepository, error) { + v4BranchProtectionRepo, err := NewBranchProtectionRepositoryV4(installationID) + if err != nil { + return nil, fmt.Errorf("initializing v4 github client failed : %v", err) + } + + v3Client, err := github.NewGithubAppClient(installationID) + if err != nil { + return nil, fmt.Errorf("initializing v3 github client failed : %v", err) + } + + combinedRepo := combinedRepositoryProvider{ + V3Repositories: v3Client.Repositories, + V4BranchProtectionRepository: v4BranchProtectionRepo, + } + + return newBranchProtectionRepository(combinedRepo, opts...), nil +} + +func newBranchProtectionRepository(combinedRepo CombinedRepository, opts ...BranchProtectionRepositoryOption) *BranchProtectionRepository { + config := &branchProtectionRepositoryConfig{} + for _, o := range opts { + o(config) + } + + if config.enableNonBlockingLimiter { + combinedRepo = NewNonBlockLimiterRepositories(combinedRepo) + } else if config.enableBlockingLimiter { + combinedRepo = NewBlockLimiterRepositories(combinedRepo) + } + + return &BranchProtectionRepository{ + combinedRepo: combinedRepo, + } +} + +// GetOwnerName retrieves the owner name of the given org and repo name +func (bp *BranchProtectionRepository) GetOwnerName(ctx context.Context, orgName, repoName string) (string, error) { + repoName = CleanGithubRepoName(repoName) + log.Debugf("GetOwnerName : getting owner name for org %s and repoName : %s", orgName, repoName) + listOpt := &githubpkg.RepositoryListByOrgOptions{ + ListOptions: githubpkg.ListOptions{ + PerPage: 30, + }, + } + for { + repos, resp, err := bp.combinedRepo.ListByOrg(ctx, orgName, listOpt) + if err != nil { + if ok, wErr := github.CheckAndWrapForKnownErrors(resp, err); ok { + return "", wErr + } + return "", err + } + + if len(repos) == 0 { + log.Warnf("GetOwnerName : no repos found under orgName : %s (maybe no access ?)", orgName) + return "", nil + } + + for _, repo := range repos { + if *repo.Name == repoName { + if repo.Owner != nil { + owner := *repo.Owner + return *owner.Login, nil + } + } + } + + // means we're at the end of it so exit + if resp.NextPage == 0 { + log.Warnf("GetOwnerName : owner name not found for org : %s and repo : %s", orgName, repoName) + return "", nil + } + + // set it to the next page + listOpt.Page = resp.NextPage + } +} + +// GetDefaultBranchForRepo helps with pulling the default branch for the given repo +func (bp *BranchProtectionRepository) GetDefaultBranchForRepo(ctx context.Context, owner, repoName string) (string, error) { + repoName = CleanGithubRepoName(repoName) + repo, resp, err := bp.combinedRepo.Get(ctx, owner, repoName) + if err != nil { + if ok, wErr := github.CheckAndWrapForKnownErrors(resp, err); ok { + return "", wErr + } + return "", err + } + + var defaultBranch string + if repo.DefaultBranch == nil { + defaultBranch = DefaultBranchName + } else { + defaultBranch = *repo.DefaultBranch + } + + return defaultBranch, nil +} + +// GetProtectedBranch fetches the protected branch details +func (bp *BranchProtectionRepository) GetProtectedBranch(ctx context.Context, owner, repoName, protectedBranchName string) (*BranchProtectionRule, error) { + repoName = CleanGithubRepoName(repoName) + branchProtections, err := bp.combinedRepo.GetRepositoryBranchProtections(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("fetching repo protections for owner : %s and repoName : %s failed : %w", owner, repoName, err) + } + + // it's not found this pattern or branch + if branchProtections.RepositoryOwner.Repository.BranchProtectionRules.TotalCount == 0 { + return nil, ErrBranchNotProtected + } + + for _, protection := range branchProtections.RepositoryOwner.Repository.BranchProtectionRules.Nodes { + if protection.Pattern == protectedBranchName { + return &protection, nil + } + } + + return nil, ErrBranchNotProtected +} + +// EnableBranchProtection enables branch protection if not enabled and makes sure passed arguments such as enforceAdmin +// statusChecks are applied. The operation makes sure it doesn't override the existing checks. +func (bp *BranchProtectionRepository) EnableBranchProtection(ctx context.Context, owner, repoName, branchName string, enforceAdmin bool, enableStatusChecks, disableStatusChecks []string) error { + repoName = CleanGithubRepoName(repoName) + + // fetch the existing ones + queryResult, err := bp.combinedRepo.GetRepositoryBranchProtections(ctx, owner, repoName) + if err != nil { + return err + } + + currentProtections := queryResult.RepositoryOwner.Repository.BranchProtectionRules.Nodes + repoID := queryResult.RepositoryOwner.Repository.ID + + createInput, updateInput := prepareBranchProtectionMutation(repoID, currentProtections, &BranchProtectionRule{ + Pattern: branchName, + RequiredStatusCheckContexts: enableStatusChecks, + RequiresStatusChecks: true, + IsAdminEnforced: enforceAdmin, + AllowsDeletions: false, + AllowsForcePushes: false, + }) + if createInput != nil { + _, createErr := bp.combinedRepo.CreateBranchProtection(ctx, createInput) + if createErr != nil { + return fmt.Errorf("creating new branch protection rule for owner : %s and repo : %s failed : %v", owner, repoName, createErr) + } + return nil + } + + _, err = bp.combinedRepo.UpdateBranchProtection(ctx, updateInput) + if err != nil { + return fmt.Errorf("updating current branch rule for owner : %s and repo name : %s, failed : %v", owner, repoName, err) + } + + return nil +} + +// mergeStatusChecks merges the current checks with the new ones and disable the ones that are specified +func mergeStatusChecks(currentChecks []string, enableContexts, disableContexts []string) []string { + + // seems github api is not happy with nils for arrays ;) + if len(enableContexts) == 0 { + enableContexts = []string{} + } + + if currentChecks == nil { + currentChecks = []string{} + } + + finalContexts := []string{} + uniqueEnableContexts := map[string]bool{} + + for _, c := range currentChecks { + // first disable the ones we're not interested into + found := false + if len(disableContexts) > 0 { + for _, disableContext := range disableContexts { + if disableContext == c { + found = true + break + } + } + } + + if found { + continue + } + + uniqueEnableContexts[c] = true + finalContexts = append(finalContexts, c) + } + + for _, c := range enableContexts { + if uniqueEnableContexts[c] { + continue + } + + uniqueEnableContexts[c] = true + finalContexts = append(finalContexts, c) + } + + return finalContexts +} + +// prepareBranchProtectionMutation creates the mutation input objects to modify the branch protection +// the logic is pulled out so we can unit test it without mocking the connections +func prepareBranchProtectionMutation(repoID string, currentProtections []BranchProtectionRule, input *BranchProtectionRule) (*githubv4.CreateBranchProtectionRuleInput, *githubv4.UpdateBranchProtectionRuleInput) { + var foundBranchProtectionRule *BranchProtectionRule + if len(currentProtections) > 0 { + for _, protection := range currentProtections { + if protection.Pattern == input.Pattern { + currentProtection := protection + foundBranchProtectionRule = ¤tProtection + break + } + } + } + + if foundBranchProtectionRule == nil { + var statusChecks []githubv4.String + for _, check := range input.RequiredStatusCheckContexts { + statusChecks = append(statusChecks, githubv4.String(check)) + } + + createInput := githubv4.CreateBranchProtectionRuleInput{ + RepositoryID: repoID, + Pattern: githubv4.String(input.Pattern), + AllowsForcePushes: githubv4.NewBoolean(false), + AllowsDeletions: githubv4.NewBoolean(false), + IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(input.IsAdminEnforced)), + RequiresStatusChecks: githubv4.NewBoolean(true), + RequiredStatusCheckContexts: &statusChecks, + } + + return &createInput, nil + } + + // it's an existing one we need to update and make sure all of them it's at state we want it + mergedStatusChecks := mergeStatusChecks(foundBranchProtectionRule.RequiredStatusCheckContexts, input.RequiredStatusCheckContexts, nil) + var finalStatusChecks []githubv4.String + + for _, check := range mergedStatusChecks { + finalStatusChecks = append(finalStatusChecks, githubv4.String(check)) + } + + updateInput := githubv4.UpdateBranchProtectionRuleInput{ + BranchProtectionRuleID: githubv4.ID(foundBranchProtectionRule.ID), + Pattern: githubv4.NewString(githubv4.String(input.Pattern)), + IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(input.IsAdminEnforced)), + RequiresStatusChecks: githubv4.NewBoolean(true), + AllowsDeletions: githubv4.NewBoolean(false), + AllowsForcePushes: githubv4.NewBoolean(false), + RequiredStatusCheckContexts: &finalStatusChecks, + } + + return nil, &updateInput +} + +// IsEnforceAdminEnabled checks if enforce admin option is enabled for the branch protection +func IsEnforceAdminEnabled(protection *BranchProtectionRule) bool { + return protection.IsAdminEnforced +} + +// CleanGithubRepoName removes the orgname if existing in the string +func CleanGithubRepoName(githubRepoName string) string { + if strings.Contains(githubRepoName, "/") { + parts := strings.Split(githubRepoName, "/") + githubRepoName = parts[len(parts)-1] + } + return githubRepoName +} diff --git a/cla-backend-go/github/branch_protection/protected_branch_limiter.go b/cla-backend-go/github/branch_protection/protected_branch_limiter.go new file mode 100644 index 000000000..ead673382 --- /dev/null +++ b/cla-backend-go/github/branch_protection/protected_branch_limiter.go @@ -0,0 +1,114 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package branch_protection + +import ( + "context" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/github" + githubpkg "github.com/google/go-github/v37/github" + "github.com/shurcooL/githubv4" + "go.uber.org/ratelimit" + "golang.org/x/time/rate" +) + +// rate limiting variables +var ( + // blockingRateLimit is useful for background tasks where the interaction is more predictable + blockingRateLimit = ratelimit.New(2) + // nonBlockingRateLimit is preferred when the github methods would be called realtime + // in this case we can call Allow method to check if can proceed or return error + nonBlockingRateLimit = rate.NewLimiter(2, 5) +) + +type blockingRateLimitRepositories struct { + CombinedRepository +} + +// NewBlockLimiterRepositories returns a new instance of V3Repositories interface with blocking rate limiting +// where when the limit is reached the next call blocks till the bucket is ready again +func NewBlockLimiterRepositories(repo CombinedRepository) CombinedRepository { + return blockingRateLimitRepositories{ + CombinedRepository: repo, + } +} + +func (b blockingRateLimitRepositories) ListByOrg(ctx context.Context, org string, opt *githubpkg.RepositoryListByOrgOptions) ([]*githubpkg.Repository, *githubpkg.Response, error) { + blockingRateLimit.Take() + return b.CombinedRepository.ListByOrg(ctx, org, opt) +} + +func (b blockingRateLimitRepositories) Get(ctx context.Context, owner, repo string) (*githubpkg.Repository, *githubpkg.Response, error) { + blockingRateLimit.Take() + return b.CombinedRepository.Get(ctx, owner, repo) +} + +func (b blockingRateLimitRepositories) GetRepositoryBranchProtections(ctx context.Context, repositoryOwner, repositoryName string) (*RepoBranchProtectionQueryResult, error) { + blockingRateLimit.Take() + return b.CombinedRepository.GetRepositoryBranchProtections(ctx, repositoryOwner, repositoryName) +} +func (b blockingRateLimitRepositories) CreateBranchProtection(ctx context.Context, input *githubv4.CreateBranchProtectionRuleInput) (*CreateRepoBranchProtectionMutation, error) { + blockingRateLimit.Take() + return b.CombinedRepository.CreateBranchProtection(ctx, input) +} +func (b blockingRateLimitRepositories) UpdateBranchProtection(ctx context.Context, input *githubv4.UpdateBranchProtectionRuleInput) (*UpdateRepoBranchProtectionMutation, error) { + blockingRateLimit.Take() + return b.CombinedRepository.UpdateBranchProtection(ctx, input) +} +func (b blockingRateLimitRepositories) GetRepositoryIDFromName(ctx context.Context, repositoryOwner, repositoryName string) (string, error) { + blockingRateLimit.Take() + return b.CombinedRepository.GetRepositoryIDFromName(ctx, repositoryOwner, repositoryName) +} + +type nonBlockingRateLimitRepositories struct { + CombinedRepository +} + +// NewNonBlockLimiterRepositories returns a new instance of V3Repositories interface with non blocking rate limiting +func NewNonBlockLimiterRepositories(repo CombinedRepository) CombinedRepository { + return nonBlockingRateLimitRepositories{CombinedRepository: repo} +} + +func (nb nonBlockingRateLimitRepositories) ListByOrg(ctx context.Context, org string, opt *githubpkg.RepositoryListByOrgOptions) ([]*githubpkg.Repository, *githubpkg.Response, error) { + if nonBlockingRateLimit.Allow() { + return nb.CombinedRepository.ListByOrg(ctx, org, opt) + } + return nil, nil, fmt.Errorf("too many requests : %w", github.ErrRateLimited) +} + +func (nb nonBlockingRateLimitRepositories) Get(ctx context.Context, owner, repo string) (*githubpkg.Repository, *githubpkg.Response, error) { + if nonBlockingRateLimit.Allow() { + return nb.CombinedRepository.Get(ctx, owner, repo) + } + return nil, nil, fmt.Errorf("too many requests : %w", github.ErrRateLimited) +} + +func (nb nonBlockingRateLimitRepositories) GetRepositoryBranchProtections(ctx context.Context, repositoryOwner, repositoryName string) (*RepoBranchProtectionQueryResult, error) { + if nonBlockingRateLimit.Allow() { + return nb.CombinedRepository.GetRepositoryBranchProtections(ctx, repositoryOwner, repositoryName) + } + return nil, fmt.Errorf("too many requests : %w", github.ErrRateLimited) +} + +func (nb nonBlockingRateLimitRepositories) CreateBranchProtection(ctx context.Context, input *githubv4.CreateBranchProtectionRuleInput) (*CreateRepoBranchProtectionMutation, error) { + if nonBlockingRateLimit.Allow() { + return nb.CombinedRepository.CreateBranchProtection(ctx, input) + } + return nil, fmt.Errorf("too many requests : %w", github.ErrRateLimited) +} + +func (nb nonBlockingRateLimitRepositories) UpdateBranchProtection(ctx context.Context, input *githubv4.UpdateBranchProtectionRuleInput) (*UpdateRepoBranchProtectionMutation, error) { + if nonBlockingRateLimit.Allow() { + return nb.CombinedRepository.UpdateBranchProtection(ctx, input) + } + return nil, fmt.Errorf("too many requests : %w", github.ErrRateLimited) +} + +func (nb nonBlockingRateLimitRepositories) GetRepositoryIDFromName(ctx context.Context, repositoryOwner, repositoryName string) (string, error) { + if nonBlockingRateLimit.Allow() { + return nb.CombinedRepository.GetRepositoryIDFromName(ctx, repositoryOwner, repositoryName) + } + return "", fmt.Errorf("too many requests : %w", github.ErrRateLimited) +} diff --git a/cla-backend-go/github/branch_protection/protected_branch_test.go b/cla-backend-go/github/branch_protection/protected_branch_test.go new file mode 100644 index 000000000..96e0ceec4 --- /dev/null +++ b/cla-backend-go/github/branch_protection/protected_branch_test.go @@ -0,0 +1,375 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package branch_protection + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/bmizerany/assert" + "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/golang/mock/gomock" + "github.com/shurcooL/githubv4" +) + +func V4StringSlice(val ...string) *[]githubv4.String { + var v []githubv4.String + for _, c := range val { + v = append(v, githubv4.String(c)) + } + + return &v +} + +// TestMergeStatusChecks tests the functionality of where we enable/disable checks +func TestMergeStatusChecks(t *testing.T) { + + testCases := []struct { + Name string + currentChecks []string + expectedChecks []string + enableContexts []string + disableContexts []string + }{ + { + Name: "all empty", + expectedChecks: []string{}, + }, + { + Name: "empty state enable", + expectedChecks: []string{"EasyCLA"}, + enableContexts: []string{"EasyCLA"}, + }, + { + Name: "preserve existing enable more", + currentChecks: []string{"travis-ci"}, + expectedChecks: []string{"travis-ci", "EasyCLA"}, + enableContexts: []string{"EasyCLA"}, + }, + { + Name: "preserve existing disable some", + currentChecks: []string{"travis-ci", "EasyCLA"}, + expectedChecks: []string{"travis-ci"}, + disableContexts: []string{"EasyCLA"}, + }, + { + Name: "add and remove in same operation", + currentChecks: []string{"travis-ci", "DCO", "EasyCLA"}, + expectedChecks: []string{"travis-ci", "EasyCLA", "CodeQL"}, + enableContexts: []string{"CodeQL"}, + disableContexts: []string{"DCO"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(tt *testing.T) { + result := mergeStatusChecks(tc.currentChecks, tc.enableContexts, tc.disableContexts) + assert.Equal(tt, tc.expectedChecks, result) + }) + } +} + +func TestEnableBranchProtection(t *testing.T) { + owner := "johnenable" + repo := "johnsrepoenable" + branchName := DefaultBranchName + + testCases := []struct { + Name string + Checks []string + CurrentProtections *RepoBranchProtectionQueryResult + CreateProtectionRequest *githubv4.CreateBranchProtectionRuleInput + UpdateProtectionRequest *githubv4.UpdateBranchProtectionRuleInput + Err error + }{ + { + Name: "success", + Checks: []string{"easyCLA"}, + CurrentProtections: &RepoBranchProtectionQueryResult{ + RepositoryOwner: struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + }{Repository: BranchProtectionRuleRepositoryParam{ + Name: "repoNameValue", + ID: "repoIDValue", + BranchProtectionRules: BranchProtectionRuleQueryParam{}, + }}, + }, + CreateProtectionRequest: &githubv4.CreateBranchProtectionRuleInput{ + RepositoryID: githubv4.ID("repoIDValue"), + Pattern: githubv4.String(branchName), + AllowsForcePushes: githubv4.NewBoolean(false), + AllowsDeletions: githubv4.NewBoolean(false), + IsAdminEnforced: githubv4.NewBoolean(true), + RequiresStatusChecks: githubv4.NewBoolean(true), + RequiredStatusCheckContexts: V4StringSlice("easyCLA"), + }, + }, + { + Name: "preserve existing checks", + Checks: []string{"easyCLA"}, + CurrentProtections: &RepoBranchProtectionQueryResult{ + RepositoryOwner: struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + }{Repository: BranchProtectionRuleRepositoryParam{ + Name: "repoNameValue", + ID: "repoIDValue", + BranchProtectionRules: BranchProtectionRuleQueryParam{ + TotalCount: 1, + Nodes: []BranchProtectionRule{ + { + ID: "branchProtectionID", + Pattern: branchName, + RequiredStatusCheckContexts: []string{ + "circle/ci", + }, + }, + }, + }, + }}, + }, + UpdateProtectionRequest: &githubv4.UpdateBranchProtectionRuleInput{ + BranchProtectionRuleID: githubv4.ID("branchProtectionID"), + Pattern: githubv4.NewString(githubv4.String(branchName)), + AllowsForcePushes: githubv4.NewBoolean(false), + AllowsDeletions: githubv4.NewBoolean(false), + IsAdminEnforced: githubv4.NewBoolean(true), + RequiresStatusChecks: githubv4.NewBoolean(true), + RequiredStatusCheckContexts: V4StringSlice("circle/ci", "easyCLA"), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(tt *testing.T) { + ctrl := gomock.NewController(tt) + defer ctrl.Finish() + + m := NewMockCombinedRepository(ctrl) + m. + EXPECT(). + GetRepositoryBranchProtections(gomock.Any(), owner, repo). + Return(tc.CurrentProtections, nil) + + if tc.CreateProtectionRequest != nil { + m. + EXPECT(). + CreateBranchProtection(gomock.Any(), tc.CreateProtectionRequest). + Return(nil, nil) + } + + if tc.UpdateProtectionRequest != nil { + m. + EXPECT(). + UpdateBranchProtection(gomock.Any(), tc.UpdateProtectionRequest). + Return(nil, nil) + } + + branchProtectionRepo := newBranchProtectionRepository(m) + err := branchProtectionRepo.EnableBranchProtection(context.Background(), owner, repo, branchName, true, tc.Checks, nil) + if err != nil { + tt.Errorf("enable branch proteciton failed : %v", err) + } + }) + } +} + +func TestNonBlockingRateLimitRepositories_GetBranchProtection(t *testing.T) { + owner := "johnblocking" + repo := "johnsrepoblocking" + branchName := DefaultBranchName + + t.Run("no limit reached", func(tt *testing.T) { + ctrl := gomock.NewController(tt) + defer ctrl.Finish() + + protection := &RepoBranchProtectionQueryResult{ + RepositoryOwner: struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + }{Repository: BranchProtectionRuleRepositoryParam{ + Name: "repoNameValue", + ID: "repoIDValue", + BranchProtectionRules: BranchProtectionRuleQueryParam{ + TotalCount: 1, + Nodes: []BranchProtectionRule{ + { + ID: "branchProtectionID", + Pattern: branchName, + RequiredStatusCheckContexts: []string{ + "circle/ci", + }, + }, + }, + }, + }}, + } + + m := NewMockCombinedRepository(ctrl) + m. + EXPECT(). + GetRepositoryBranchProtections(gomock.Any(), owner, repo). + Return(protection, nil) + + nonBlockLimitRepo := newBranchProtectionRepository(m, EnableNonBlockingLimiter()) + p, err := nonBlockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) + if err != nil { + tt.Errorf("no error expected : %v", err) + } + assert.Equal(tt, protection.RepositoryOwner.Repository.BranchProtectionRules.Nodes[0], *p) + }) + + t.Run("limit reached", func(tt *testing.T) { + ctrl := gomock.NewController(tt) + defer ctrl.Finish() + + protection := &RepoBranchProtectionQueryResult{ + RepositoryOwner: struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + }{Repository: BranchProtectionRuleRepositoryParam{ + Name: "repoNameValue", + ID: "repoIDValue", + BranchProtectionRules: BranchProtectionRuleQueryParam{ + TotalCount: 1, + Nodes: []BranchProtectionRule{ + { + ID: "branchProtectionID", + Pattern: branchName, + RequiredStatusCheckContexts: []string{ + "circle/ci", + }, + }, + }, + }, + }}, + } + + m := NewMockCombinedRepository(ctrl) + m. + EXPECT(). + GetRepositoryBranchProtections(gomock.Any(), owner, repo). + Return(protection, nil).AnyTimes() + + nonBlockLimitRepo := newBranchProtectionRepository(m, EnableNonBlockingLimiter()) + // call it 100 times in loop to make it fail + var expectedErr error + for i := 0; i < 100; i++ { + _, err := nonBlockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) + if err != nil { + expectedErr = err + break + } + } + + if expectedErr == nil { + tt.Fatalf("no error returned") + return + } + + if !errors.Is(expectedErr, github.ErrRateLimited) { + tt.Fatalf("was expecting ErrRateLimited got : %v", expectedErr) + return + } + }) +} + +func TestBlockingRateLimitRepositories_GetBranchProtection(t *testing.T) { + owner := "john" + repo := "johnsrepo" + branchName := DefaultBranchName + + t.Run("no limit reached", func(tt *testing.T) { + ctrl := gomock.NewController(tt) + defer ctrl.Finish() + + protection := &RepoBranchProtectionQueryResult{ + RepositoryOwner: struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + }{Repository: BranchProtectionRuleRepositoryParam{ + Name: "repoNameValue", + ID: "repoIDValue", + BranchProtectionRules: BranchProtectionRuleQueryParam{ + TotalCount: 1, + Nodes: []BranchProtectionRule{ + { + ID: "branchProtectionID", + Pattern: branchName, + RequiredStatusCheckContexts: []string{ + "circle/ci", + }, + }, + }, + }, + }}, + } + + m := NewMockCombinedRepository(ctrl) + m. + EXPECT(). + GetRepositoryBranchProtections(gomock.Any(), owner, repo). + Return(protection, nil) + + blockLimitRepo := newBranchProtectionRepository(m, EnableBlockingLimiter()) + p, err := blockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) + if err != nil { + tt.Errorf("no error expected : %v", err) + } + assert.Equal(tt, protection.RepositoryOwner.Repository.BranchProtectionRules.Nodes[0], *p) + }) + + t.Run("limit reached", func(tt *testing.T) { + ctrl := gomock.NewController(tt) + defer ctrl.Finish() + + protection := &RepoBranchProtectionQueryResult{ + RepositoryOwner: struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + }{Repository: BranchProtectionRuleRepositoryParam{ + Name: "repoNameValue", + ID: "repoIDValue", + BranchProtectionRules: BranchProtectionRuleQueryParam{ + TotalCount: 1, + Nodes: []BranchProtectionRule{ + { + ID: "branchProtectionID", + Pattern: branchName, + RequiredStatusCheckContexts: []string{ + "circle/ci", + }, + }, + }, + }, + }}, + } + + m := NewMockCombinedRepository(ctrl) + m. + EXPECT(). + GetRepositoryBranchProtections(gomock.Any(), owner, repo). + Return(protection, nil).AnyTimes() + + blockLimitRepo := newBranchProtectionRepository(m, EnableBlockingLimiter()) + + // call it 100 times in loop to make it fail + var expectedErr error + start := time.Now() + for i := 0; i < 10; i++ { + _, err := blockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) + if err != nil { + expectedErr = err + break + } + } + elapsed := time.Since(start) + + if expectedErr != nil { + tt.Fatalf("no error was expected got : %v", expectedErr) + return + } + + if elapsed < 4*time.Second { + tt.Fatalf("is rate limit enabled") + } + }) +} diff --git a/cla-backend-go/github/branch_protection/protected_branch_v4.go b/cla-backend-go/github/branch_protection/protected_branch_v4.go new file mode 100644 index 000000000..935354870 --- /dev/null +++ b/cla-backend-go/github/branch_protection/protected_branch_v4.go @@ -0,0 +1,163 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package branch_protection + +import ( + "context" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/shurcooL/githubv4" +) + +// BranchProtectionRule is the data structure that's used to reflect the remote github branch protection rule +type BranchProtectionRule struct { + ID string + Pattern string + RequiredStatusCheckContexts []string + RequiresStatusChecks bool + IsAdminEnforced bool + AllowsDeletions bool + AllowsForcePushes bool +} + +// BranchProtectionRuleQueryParam is part of RepoBranchProtectionQueryResult extracted here so can +// easily be initialized +type BranchProtectionRuleQueryParam struct { + TotalCount int + Nodes []BranchProtectionRule +} + +// BranchProtectionRuleRepositoryParam is part of RepoBranchProtectionQueryResult extracted here so can +// easily be initialized +type BranchProtectionRuleRepositoryParam struct { + Name string + ID string + BranchProtectionRules BranchProtectionRuleQueryParam `graphql:"branchProtectionRules(first:10)"` +} + +// RepoBranchProtectionQueryResult is the query which queries for given owner and repository name +type RepoBranchProtectionQueryResult struct { + RepositoryOwner struct { + Repository BranchProtectionRuleRepositoryParam `graphql:"repository(name: $name)"` + } `graphql:"repositoryOwner(login: $login)"` +} + +// CreateRepoBranchProtectionMutation adds a new branch protection rule +type CreateRepoBranchProtectionMutation struct { + CreateBranchProtectionRule struct { + BranchProtectionRule struct { + Repository struct { + Name string + } + Pattern string + IsAdminEnforced bool + RequiresStatusChecks bool + AllowsDeletions bool + AllowsForcePushes bool + } + } `graphql:"createBranchProtectionRule(input: $input)"` +} + +// UpdateRepoBranchProtectionMutation updates existing branch protection rule +type UpdateRepoBranchProtectionMutation struct { + UpdateBranchProtectionRule struct { + BranchProtectionRule struct { + Repository struct { + Name string + } + Pattern string + IsAdminEnforced bool + RequiresStatusChecks bool + AllowsDeletions bool + AllowsForcePushes bool + } + } `graphql:"updateBranchProtectionRule(input: $input)"` +} + +// BranchProtectionRepositoryV4 wraps a v4 github client +type BranchProtectionRepositoryV4 struct { + client *githubv4.Client +} + +// NewBranchProtectionRepositoryV4 creates a new BranchProtectionRepositoryV4 +func NewBranchProtectionRepositoryV4(installationID int64) (*BranchProtectionRepositoryV4, error) { + client, clientErr := github.NewGithubV4AppClient(installationID) + if clientErr != nil { + return nil, clientErr + } + return &BranchProtectionRepositoryV4{ + client: client, + }, nil +} + +// GetRepositoryBranchProtections returns the repository branch protections for the specified repository +func (r *BranchProtectionRepositoryV4) GetRepositoryBranchProtections(ctx context.Context, repositoryOwner, repositoryName string) (*RepoBranchProtectionQueryResult, error) { + var queryResult RepoBranchProtectionQueryResult + + variables := map[string]interface{}{ + "login": githubv4.String(repositoryOwner), + "name": githubv4.String(repositoryName), + } + + err := r.client.Query(ctx, &queryResult, variables) + if err != nil { + return nil, fmt.Errorf("fetching branch protection rules for owner : %s and repo : %s failed : %v", repositoryOwner, repositoryName, err) + } + + return &queryResult, nil +} + +// CreateBranchProtection creates the repository branch protections for the specified repository +func (r *BranchProtectionRepositoryV4) CreateBranchProtection(ctx context.Context, input *githubv4.CreateBranchProtectionRuleInput) (*CreateRepoBranchProtectionMutation, error) { + var createMutationResult CreateRepoBranchProtectionMutation + err := r.client.Mutate(ctx, &createMutationResult, *input, nil) + if err != nil { + return nil, fmt.Errorf("creating new branch protection failed : %w", err) + } + return &createMutationResult, nil +} + +// UpdateBranchProtection updates the repository branch protections for the specified repository +func (r *BranchProtectionRepositoryV4) UpdateBranchProtection(ctx context.Context, input *githubv4.UpdateBranchProtectionRuleInput) (*UpdateRepoBranchProtectionMutation, error) { + var updateMutationResult UpdateRepoBranchProtectionMutation + err := r.client.Mutate(ctx, &updateMutationResult, *input, nil) + if err != nil { + return nil, fmt.Errorf("updating current branch rule failed : %w", err) + } + return &updateMutationResult, nil +} + +// GetRepositoryIDFromName when provided the organization and repository name, returns the repository ID +func (r *BranchProtectionRepositoryV4) GetRepositoryIDFromName(ctx context.Context, repositoryOwner, repositoryName string) (string, error) { + + // Define the graphql query + //"query": "query{repository(name: \"test1\", owner: \"deal-test-org\") {id}}" + var query struct { + Viewer struct { + Login githubv4.String + } + Repository struct { + ID string + } `graphql:"repository(owner:$repositoryOwner, name:$repositoryName)"` + } + + // Define the variables for the query + variables := map[string]interface{}{ + "repositoryOwner": githubv4.String(repositoryOwner), + "repositoryName": githubv4.String(repositoryName), + } + + err := r.client.Query(ctx, &query, variables) + if err != nil { + return "", err + } + + return query.Repository.ID, nil +} + +// EnableBranchProtectionForPattern enables branch protection for the given branch protection input +func EnableBranchProtectionForPattern(ctx context.Context, repositoryOwner, repositoryName string, input *BranchProtectionRule) error { + return nil +} diff --git a/cla-backend-go/github/client.go b/cla-backend-go/github/client.go index e6efd8591..7b1c84f05 100644 --- a/cla-backend-go/github/client.go +++ b/cla-backend-go/github/client.go @@ -8,9 +8,12 @@ import ( "errors" "fmt" "net/http" + "time" - "github.com/bradleyfalzon/ghinstallation" - "github.com/google/go-github/v33/github" + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/shurcooL/githubv4" + + "github.com/google/go-github/v37/github" "golang.org/x/oauth2" ) @@ -53,7 +56,8 @@ func isGithubRateLimit(err error) (bool, error) { return false, nil } -func checkAndWrapForKnownErrors(resp *github.Response, err error) (bool, error) { +// CheckAndWrapForKnownErrors checks for some of the known error types +func CheckAndWrapForKnownErrors(resp *github.Response, err error) (bool, error) { if err == nil { return false, err } @@ -77,6 +81,15 @@ func NewGithubAppClient(installationID int64) (*github.Client, error) { return github.NewClient(&http.Client{Transport: itr}), nil } +// NewGithubV4AppClient creates a new github v4 client from the supplied installationID +func NewGithubV4AppClient(installationID int64) (*githubv4.Client, error) { + authTransport, err := ghinstallation.New(http.DefaultTransport, int64(getGithubAppID()), installationID, []byte(getGithubAppPrivateKey())) + if err != nil { + return nil, err + } + return githubv4.NewClient(&http.Client{Transport: authTransport, Timeout: 5 * time.Second}), nil +} + // NewGithubOauthClient creates github client from global accessToken func NewGithubOauthClient() *github.Client { return NewGithubOauthClientWithAccessToken(getSecretAccessToken()) diff --git a/cla-backend-go/github/github-query.graphql b/cla-backend-go/github/github-query.graphql new file mode 100644 index 000000000..4cf4ccb40 --- /dev/null +++ b/cla-backend-go/github/github-query.graphql @@ -0,0 +1,25 @@ +query BranchProtectionRule($organizationOwner: String!, $repositoryName: String!) { + repository(owner: $organizationOwner, name: $repositoryName) { + id + createdAt + branchProtectionRules(first: 100) { + totalCount + nodes { + pattern + id + allowsDeletions + requiredApprovingReviewCount + requiredStatusCheckContexts + } + edges { + node { + allowsDeletions + id + pattern + } + } + } + diskUsage + hasIssuesEnabled + } +} diff --git a/cla-backend-go/github/github-schema.graphql b/cla-backend-go/github/github-schema.graphql new file mode 100644 index 000000000..3ae15a1a7 --- /dev/null +++ b/cla-backend-go/github/github-schema.graphql @@ -0,0 +1,39977 @@ +""" +Defines what type of global IDs are accepted for a mutation argument of type ID. +""" +directive @possibleTypes( + """ + Abstract type of accepted global ID + """ + abstractType: String + + """ + Accepted types of global IDs. + """ + concreteTypes: [String!]! +) on INPUT_FIELD_DEFINITION + +""" +Marks an element of a GraphQL schema as only available via a preview header +""" +directive @preview( + """ + The identifier of the API preview that toggles this field. + """ + toggledBy: String! +) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +""" +Autogenerated input type of AcceptEnterpriseAdministratorInvitation +""" +input AcceptEnterpriseAdministratorInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the invitation being accepted + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseAdministratorInvitation"]) +} + +""" +Autogenerated return type of AcceptEnterpriseAdministratorInvitation +""" +type AcceptEnterpriseAdministratorInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was accepted. + """ + invitation: EnterpriseAdministratorInvitation + + """ + A message confirming the result of accepting an administrator invitation. + """ + message: String +} + +""" +Autogenerated input type of AcceptTopicSuggestion +""" +input AcceptTopicSuggestionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the suggested topic. + """ + name: String! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of AcceptTopicSuggestion +""" +type AcceptTopicSuggestionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The accepted topic. + """ + topic: Topic +} + +""" +Represents an object which can take actions on GitHub. Typically a User or Bot. +""" +interface Actor { + """ + A URL pointing to the actor's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The username of the actor. + """ + login: String! + + """ + The HTTP path for this actor. + """ + resourcePath: URI! + + """ + The HTTP URL for this actor. + """ + url: URI! +} + +""" +Location information for an actor +""" +type ActorLocation { + """ + City + """ + city: String + + """ + Country name + """ + country: String + + """ + Country code + """ + countryCode: String + + """ + Region name + """ + region: String + + """ + Region or state code + """ + regionCode: String +} + +""" +Autogenerated input type of AddAssigneesToAssignable +""" +input AddAssigneesToAssignableInput { + """ + The id of the assignable object to add assignees to. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + The id of users to add as assignees. + """ + assigneeIds: [ID!]! @possibleTypes(concreteTypes: ["User"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of AddAssigneesToAssignable +""" +type AddAssigneesToAssignablePayload { + """ + The item that was assigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of AddComment +""" +input AddCommentInput { + """ + The contents of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") +} + +""" +Autogenerated return type of AddComment +""" +type AddCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The edge from the subject's comment connection. + """ + commentEdge: IssueCommentEdge + + """ + The subject + """ + subject: Node + + """ + The edge from the subject's timeline connection. + """ + timelineEdge: IssueTimelineItemEdge +} + +""" +Autogenerated input type of AddEnterpriseSupportEntitlement +""" +input AddEnterpriseSupportEntitlementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a member who will receive the support entitlement. + """ + login: String! +} + +""" +Autogenerated return type of AddEnterpriseSupportEntitlement +""" +type AddEnterpriseSupportEntitlementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of adding the support entitlement. + """ + message: String +} + +""" +Autogenerated input type of AddLabelsToLabelable +""" +input AddLabelsToLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ids of the labels to add. + """ + labelIds: [ID!]! @possibleTypes(concreteTypes: ["Label"]) + + """ + The id of the labelable object to add labels to. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of AddLabelsToLabelable +""" +type AddLabelsToLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was labeled. + """ + labelable: Labelable +} + +""" +Autogenerated input type of AddProjectCard +""" +input AddProjectCardInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The content of the card. Must be a member of the ProjectCardItem union + """ + contentId: ID @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "ProjectCardItem") + + """ + The note on the card. + """ + note: String + + """ + The Node ID of the ProjectColumn. + """ + projectColumnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of AddProjectCard +""" +type AddProjectCardPayload { + """ + The edge from the ProjectColumn's card connection. + """ + cardEdge: ProjectCardEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ProjectColumn + """ + projectColumn: ProjectColumn +} + +""" +Autogenerated input type of AddProjectColumn +""" +input AddProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the column. + """ + name: String! + + """ + The Node ID of the project. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) +} + +""" +Autogenerated return type of AddProjectColumn +""" +type AddProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The edge from the project's column connection. + """ + columnEdge: ProjectColumnEdge + + """ + The project + """ + project: Project +} + +""" +Autogenerated input type of AddPullRequestReviewComment +""" +input AddPullRequestReviewCommentInput { + """ + The text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The SHA of the commit to comment on. + """ + commitOID: GitObjectID + + """ + The comment id to reply to. + """ + inReplyTo: ID @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) + + """ + The relative path of the file to comment on. + """ + path: String + + """ + The line index in the diff to comment on. + """ + position: Int + + """ + The node ID of the pull request reviewing + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node ID of the review to modify. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of AddPullRequestReviewComment +""" +type AddPullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created comment. + """ + comment: PullRequestReviewComment + + """ + The edge from the review's comment connection. + """ + commentEdge: PullRequestReviewCommentEdge +} + +""" +Autogenerated input type of AddPullRequestReview +""" +input AddPullRequestReviewInput { + """ + The contents of the review body comment. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The review line comments. + """ + comments: [DraftPullRequestReviewComment] + + """ + The commit OID the review pertains to. + """ + commitOID: GitObjectID + + """ + The event to perform on the pull request review. + """ + event: PullRequestReviewEvent + + """ + The Node ID of the pull request to modify. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The review line comment threads. + """ + threads: [DraftPullRequestReviewThread] +} + +""" +Autogenerated return type of AddPullRequestReview +""" +type AddPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created pull request review. + """ + pullRequestReview: PullRequestReview + + """ + The edge from the pull request's review connection. + """ + reviewEdge: PullRequestReviewEdge +} + +""" +Autogenerated input type of AddPullRequestReviewThread +""" +input AddPullRequestReviewThreadInput { + """ + Body of the thread's first comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The line of the blob to which the thread refers. The end of the line range for multi-line comments. + """ + line: Int! + + """ + Path to the file being commented on. + """ + path: String! + + """ + The node ID of the pull request reviewing + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node ID of the review to modify. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) + + """ + The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. + """ + side: DiffSide = RIGHT + + """ + The first line of the range to which the comment refers. + """ + startLine: Int + + """ + The side of the diff on which the start line resides. + """ + startSide: DiffSide = RIGHT +} + +""" +Autogenerated return type of AddPullRequestReviewThread +""" +type AddPullRequestReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created thread. + """ + thread: PullRequestReviewThread +} + +""" +Autogenerated input type of AddReaction +""" +input AddReactionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the emoji to react with. + """ + content: ReactionContent! + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["CommitComment", "Issue", "IssueComment", "PullRequest", "PullRequestReview", "PullRequestReviewComment", "TeamDiscussion", "TeamDiscussionComment"], abstractType: "Reactable") +} + +""" +Autogenerated return type of AddReaction +""" +type AddReactionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reaction object. + """ + reaction: Reaction + + """ + The reactable subject. + """ + subject: Reactable +} + +""" +Autogenerated input type of AddStar +""" +input AddStarInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Starrable ID to star. + """ + starrableId: ID! @possibleTypes(concreteTypes: ["Gist", "Repository", "Topic"], abstractType: "Starrable") +} + +""" +Autogenerated return type of AddStar +""" +type AddStarPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The starrable. + """ + starrable: Starrable +} + +""" +Autogenerated input type of AddVerifiableDomain +""" +input AddVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The URL of the domain + """ + domain: URI! + + """ + The ID of the owner to add the domain to + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "VerifiableDomainOwner") +} + +""" +Autogenerated return type of AddVerifiableDomain +""" +type AddVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was added. + """ + domain: VerifiableDomain +} + +""" +Represents a 'added_to_project' event on a given issue or pull request. +""" +type AddedToProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard @preview(toggledBy: "starfox-preview") + + """ + Column name referenced by this project event. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +A GitHub App. +""" +type App implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the app. + """ + description: String + id: ID! + + """ + The hex color code, without the leading '#', for the logo background. + """ + logoBackgroundColor: String! + + """ + A URL pointing to the app's logo. + """ + logoUrl( + """ + The size of the resulting image. + """ + size: Int + ): URI! + + """ + The name of the app. + """ + name: String! + + """ + A slug based on the name of the app for use in URLs. + """ + slug: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The URL to the app's homepage. + """ + url: URI! +} + +""" +Autogenerated input type of ArchiveRepository +""" +input ArchiveRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to mark as archived. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of ArchiveRepository +""" +type ArchiveRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was marked as archived. + """ + repository: Repository +} + +""" +An object that can have users assigned to it. +""" +interface Assignable { + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! +} + +""" +Represents an 'assigned' event on any assignable object. +""" +type AssignedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the assignable associated with the event. + """ + assignable: Assignable! + + """ + Identifies the user or mannequin that was assigned. + """ + assignee: Assignee + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the user who was assigned. + """ + user: User @deprecated(reason: "Assignees can now be mannequins. Use the `assignee` field instead. Removal on 2020-01-01 UTC.") +} + +""" +Types that can be assigned to issues. +""" +union Assignee = Bot | Mannequin | Organization | User + +""" +An entry in the audit log. +""" +interface AuditEntry { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Types that can initiate an audit log event. +""" +union AuditEntryActor = Bot | Organization | User + +""" +Ordering options for Audit Log connections. +""" +input AuditLogOrder { + """ + The ordering direction. + """ + direction: OrderDirection + + """ + The field to order Audit Logs by. + """ + field: AuditLogOrderField +} + +""" +Properties by which Audit Log connections can be ordered. +""" +enum AuditLogOrderField { + """ + Order audit log entries by timestamp + """ + CREATED_AT +} + +""" +Represents a 'auto_merge_disabled' event on a given pull request. +""" +type AutoMergeDisabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who disabled auto-merge for this Pull Request + """ + disabler: User + id: ID! + + """ + PullRequest referenced by event + """ + pullRequest: PullRequest + + """ + The reason auto-merge was disabled + """ + reason: String + + """ + The reason_code relating to why auto-merge was disabled + """ + reasonCode: String +} + +""" +Represents a 'auto_merge_enabled' event on a given pull request. +""" +type AutoMergeEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge for this Pull Request + """ + enabler: User + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'auto_rebase_enabled' event on a given pull request. +""" +type AutoRebaseEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge (rebase) for this Pull Request + """ + enabler: User + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'auto_squash_enabled' event on a given pull request. +""" +type AutoSquashEnabledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The user who enabled auto-merge (squash) for this Pull Request + """ + enabler: User + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'automatic_base_change_failed' event on a given pull request. +""" +type AutomaticBaseChangeFailedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The new base for this PR + """ + newBase: String! + + """ + The old base for this PR + """ + oldBase: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'automatic_base_change_succeeded' event on a given pull request. +""" +type AutomaticBaseChangeSucceededEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The new base for this PR + """ + newBase: String! + + """ + The old base for this PR + """ + oldBase: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'base_ref_changed' event on a given issue or pull request. +""" +type BaseRefChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the name of the base ref for the pull request after it was changed. + """ + currentRefName: String! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Identifies the name of the base ref for the pull request before it was changed. + """ + previousRefName: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'base_ref_deleted' event on a given pull request. +""" +type BaseRefDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the name of the Ref associated with the `base_ref_deleted` event. + """ + baseRefName: String + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest +} + +""" +Represents a 'base_ref_force_pushed' event on a given pull request. +""" +type BaseRefForcePushedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the after commit SHA for the 'base_ref_force_pushed' event. + """ + afterCommit: Commit + + """ + Identifies the before commit SHA for the 'base_ref_force_pushed' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the fully qualified ref name for the 'base_ref_force_pushed' event. + """ + ref: Ref +} + +""" +Represents a Git blame. +""" +type Blame { + """ + The list of ranges from a Git blame. + """ + ranges: [BlameRange!]! +} + +""" +Represents a range of information from a Git blame. +""" +type BlameRange { + """ + Identifies the recency of the change, from 1 (new) to 10 (old). This is + calculated as a 2-quantile and determines the length of distance between the + median age of all the changes in the file and the recency of the current + range's change. + """ + age: Int! + + """ + Identifies the line author + """ + commit: Commit! + + """ + The ending line for the range + """ + endingLine: Int! + + """ + The starting line for the range + """ + startingLine: Int! +} + +""" +Represents a Git blob. +""" +type Blob implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + Byte size of Blob object + """ + byteSize: Int! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + id: ID! + + """ + Indicates whether the Blob is binary or text. Returns null if unable to determine the encoding. + """ + isBinary: Boolean + + """ + Indicates whether the contents is truncated + """ + isTruncated: Boolean! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! + + """ + UTF8 text data or null if the Blob is binary + """ + text: String +} + +""" +A special type of user which takes actions on behalf of GitHub Apps. +""" +type Bot implements Actor & Node & UniformResourceLocatable { + """ + A URL pointing to the GitHub App's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The username of the actor. + """ + login: String! + + """ + The HTTP path for this bot + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this bot + """ + url: URI! +} + +""" +A branch protection rule. +""" +type BranchProtectionRule implements Node { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean! + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean! + + """ + A list of conflicts matching branches protection rule and other branch protection rules + """ + branchProtectionRuleConflicts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BranchProtectionRuleConflictConnection! + + """ + The actor who created this branch protection rule. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean! + id: ID! + + """ + Can admins overwrite branch protection. + """ + isAdminEnforced: Boolean! + + """ + Repository refs that are protected by this rule + """ + matchingRefs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters refs with query on name + """ + query: String + ): RefConnection! + + """ + Identifies the protection rule pattern. + """ + pattern: String! + + """ + A list push allowances for this branch protection rule. + """ + pushAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PushAllowanceConnection! + + """ + The repository associated with this branch protection rule. + """ + repository: Repository + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean! + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean! + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean! + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean! + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean! + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean! + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean! + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean! + + """ + A list review dismissal allowances for this branch protection rule. + """ + reviewDismissalAllowances( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReviewDismissalAllowanceConnection! +} + +""" +A conflict between two branch protection rules. +""" +type BranchProtectionRuleConflict { + """ + Identifies the branch protection rule. + """ + branchProtectionRule: BranchProtectionRule + + """ + Identifies the conflicting branch protection rule. + """ + conflictingBranchProtectionRule: BranchProtectionRule + + """ + Identifies the branch ref that has conflicting rules + """ + ref: Ref +} + +""" +The connection type for BranchProtectionRuleConflict. +""" +type BranchProtectionRuleConflictConnection { + """ + A list of edges. + """ + edges: [BranchProtectionRuleConflictEdge] + + """ + A list of nodes. + """ + nodes: [BranchProtectionRuleConflict] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BranchProtectionRuleConflictEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BranchProtectionRuleConflict +} + +""" +The connection type for BranchProtectionRule. +""" +type BranchProtectionRuleConnection { + """ + A list of edges. + """ + edges: [BranchProtectionRuleEdge] + + """ + A list of nodes. + """ + nodes: [BranchProtectionRule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type BranchProtectionRuleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: BranchProtectionRule +} + +""" +Autogenerated input type of CancelEnterpriseAdminInvitation +""" +input CancelEnterpriseAdminInvitationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pending enterprise administrator invitation. + """ + invitationId: ID! @possibleTypes(concreteTypes: ["EnterpriseAdministratorInvitation"]) +} + +""" +Autogenerated return type of CancelEnterpriseAdminInvitation +""" +type CancelEnterpriseAdminInvitationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The invitation that was canceled. + """ + invitation: EnterpriseAdministratorInvitation + + """ + A message confirming the result of canceling an administrator invitation. + """ + message: String +} + +""" +Autogenerated input type of ChangeUserStatus +""" +input ChangeUserStatusInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The emoji to represent your status. Can either be a native Unicode emoji or an emoji name with colons, e.g., :grinning:. + """ + emoji: String + + """ + If set, the user status will not be shown after this date. + """ + expiresAt: DateTime + + """ + Whether this status should indicate you are not fully available on GitHub, e.g., you are away. + """ + limitedAvailability: Boolean = false + + """ + A short description of your current status. + """ + message: String + + """ + The ID of the organization whose members will be allowed to see the status. If + omitted, the status will be publicly visible. + """ + organizationId: ID @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of ChangeUserStatus +""" +type ChangeUserStatusPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Your updated status. + """ + status: UserStatus +} + +""" +A single check annotation. +""" +type CheckAnnotation { + """ + The annotation's severity level. + """ + annotationLevel: CheckAnnotationLevel + + """ + The path to the file that this annotation was made on. + """ + blobUrl: URI! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The position of this annotation. + """ + location: CheckAnnotationSpan! + + """ + The annotation's message. + """ + message: String! + + """ + The path that this annotation was made on. + """ + path: String! + + """ + Additional information about the annotation. + """ + rawDetails: String + + """ + The annotation's title + """ + title: String +} + +""" +The connection type for CheckAnnotation. +""" +type CheckAnnotationConnection { + """ + A list of edges. + """ + edges: [CheckAnnotationEdge] + + """ + A list of nodes. + """ + nodes: [CheckAnnotation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Information from a check run analysis to specific lines of code. +""" +input CheckAnnotationData { + """ + Represents an annotation's information level + """ + annotationLevel: CheckAnnotationLevel! + + """ + The location of the annotation + """ + location: CheckAnnotationRange! + + """ + A short description of the feedback for these lines of code. + """ + message: String! + + """ + The path of the file to add an annotation to. + """ + path: String! + + """ + Details about this annotation. + """ + rawDetails: String + + """ + The title that represents the annotation. + """ + title: String +} + +""" +An edge in a connection. +""" +type CheckAnnotationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckAnnotation +} + +""" +Represents an annotation's information level. +""" +enum CheckAnnotationLevel { + """ + An annotation indicating an inescapable error. + """ + FAILURE + + """ + An annotation indicating some information. + """ + NOTICE + + """ + An annotation indicating an ignorable error. + """ + WARNING +} + +""" +A character position in a check annotation. +""" +type CheckAnnotationPosition { + """ + Column number (1 indexed). + """ + column: Int + + """ + Line number (1 indexed). + """ + line: Int! +} + +""" +Information from a check run analysis to specific lines of code. +""" +input CheckAnnotationRange { + """ + The ending column of the range. + """ + endColumn: Int + + """ + The ending line of the range. + """ + endLine: Int! + + """ + The starting column of the range. + """ + startColumn: Int + + """ + The starting line of the range. + """ + startLine: Int! +} + +""" +An inclusive pair of positions for a check annotation. +""" +type CheckAnnotationSpan { + """ + End position (inclusive). + """ + end: CheckAnnotationPosition! + + """ + Start position (inclusive). + """ + start: CheckAnnotationPosition! +} + +""" +The possible states for a check suite or run conclusion. +""" +enum CheckConclusionState { + """ + The check suite or run requires action. + """ + ACTION_REQUIRED + + """ + The check suite or run has been cancelled. + """ + CANCELLED + + """ + The check suite or run has failed. + """ + FAILURE + + """ + The check suite or run was neutral. + """ + NEUTRAL + + """ + The check suite or run was skipped. + """ + SKIPPED + + """ + The check suite or run was marked stale by GitHub. Only GitHub can use this conclusion. + """ + STALE + + """ + The check suite or run has failed at startup. + """ + STARTUP_FAILURE + + """ + The check suite or run has succeeded. + """ + SUCCESS + + """ + The check suite or run has timed out. + """ + TIMED_OUT +} + +""" +A check run. +""" +type CheckRun implements Node & UniformResourceLocatable { + """ + The check run's annotations + """ + annotations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckAnnotationConnection + + """ + The check suite that this run is a part of. + """ + checkSuite: CheckSuite! + + """ + Identifies the date and time when the check run was completed. + """ + completedAt: DateTime + + """ + The conclusion of the check run. + """ + conclusion: CheckConclusionState + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The URL from which to find full details of the check run on the integrator's site. + """ + detailsUrl: URI + + """ + A reference for the check run on the integrator's system. + """ + externalId: String + id: ID! + + """ + The name of the check for this check run. + """ + name: String! + + """ + The permalink to the check run summary. + """ + permalink: URI! + + """ + The repository associated with this check run. + """ + repository: Repository! + + """ + The HTTP path for this check run. + """ + resourcePath: URI! + + """ + Identifies the date and time when the check run was started. + """ + startedAt: DateTime + + """ + The current status of the check run. + """ + status: CheckStatusState! + + """ + A string representing the check run's summary + """ + summary: String + + """ + A string representing the check run's text + """ + text: String + + """ + A string representing the check run + """ + title: String + + """ + The HTTP URL for this check run. + """ + url: URI! +} + +""" +Possible further actions the integrator can perform. +""" +input CheckRunAction { + """ + A short explanation of what this action would do. + """ + description: String! + + """ + A reference for the action on the integrator's system. + """ + identifier: String! + + """ + The text to be displayed on a button in the web UI. + """ + label: String! +} + +""" +The connection type for CheckRun. +""" +type CheckRunConnection { + """ + A list of edges. + """ + edges: [CheckRunEdge] + + """ + A list of nodes. + """ + nodes: [CheckRun] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckRunEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckRun +} + +""" +The filters that are available when fetching check runs. +""" +input CheckRunFilter { + """ + Filters the check runs created by this application ID. + """ + appId: Int + + """ + Filters the check runs by this name. + """ + checkName: String + + """ + Filters the check runs by this type. + """ + checkType: CheckRunType + + """ + Filters the check runs by this status. + """ + status: CheckStatusState +} + +""" +Descriptive details about the check run. +""" +input CheckRunOutput { + """ + The annotations that are made as part of the check run. + """ + annotations: [CheckAnnotationData!] + + """ + Images attached to the check run output displayed in the GitHub pull request UI. + """ + images: [CheckRunOutputImage!] + + """ + The summary of the check run (supports Commonmark). + """ + summary: String! + + """ + The details of the check run (supports Commonmark). + """ + text: String + + """ + A title to provide for this check run. + """ + title: String! +} + +""" +Images attached to the check run output displayed in the GitHub pull request UI. +""" +input CheckRunOutputImage { + """ + The alternative text for the image. + """ + alt: String! + + """ + A short image description. + """ + caption: String + + """ + The full URL of the image. + """ + imageUrl: URI! +} + +""" +The possible types of check runs. +""" +enum CheckRunType { + """ + Every check run available. + """ + ALL + + """ + The latest check run. + """ + LATEST +} + +""" +The possible states for a check suite or run status. +""" +enum CheckStatusState { + """ + The check suite or run has been completed. + """ + COMPLETED + + """ + The check suite or run is in progress. + """ + IN_PROGRESS + + """ + The check suite or run has been queued. + """ + QUEUED + + """ + The check suite or run has been requested. + """ + REQUESTED + + """ + The check suite or run is in waiting state. + """ + WAITING +} + +""" +A check suite. +""" +type CheckSuite implements Node { + """ + The GitHub App which created this check suite. + """ + app: App + + """ + The name of the branch for this check suite. + """ + branch: Ref + + """ + The check runs associated with a check suite. + """ + checkRuns( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filters the check runs by this type. + """ + filterBy: CheckRunFilter + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckRunConnection + + """ + The commit for this check suite + """ + commit: Commit! + + """ + The conclusion of this check suite. + """ + conclusion: CheckConclusionState + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + A list of open pull requests matching the check suite. + """ + matchingPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection + + """ + The push that triggered this check suite. + """ + push: Push + + """ + The repository associated with this check suite. + """ + repository: Repository! + + """ + The HTTP path for this check suite + """ + resourcePath: URI! + + """ + The status of this check suite. + """ + status: CheckStatusState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this check suite + """ + url: URI! +} + +""" +The auto-trigger preferences that are available for check suites. +""" +input CheckSuiteAutoTriggerPreference { + """ + The node ID of the application that owns the check suite. + """ + appId: ID! + + """ + Set to `true` to enable automatic creation of CheckSuite events upon pushes to the repository. + """ + setting: Boolean! +} + +""" +The connection type for CheckSuite. +""" +type CheckSuiteConnection { + """ + A list of edges. + """ + edges: [CheckSuiteEdge] + + """ + A list of nodes. + """ + nodes: [CheckSuite] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CheckSuiteEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CheckSuite +} + +""" +The filters that are available when fetching check suites. +""" +input CheckSuiteFilter { + """ + Filters the check suites created by this application ID. + """ + appId: Int + + """ + Filters the check suites by this name. + """ + checkName: String +} + +""" +Autogenerated input type of ClearLabelsFromLabelable +""" +input ClearLabelsFromLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the labelable object to clear the labels from. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of ClearLabelsFromLabelable +""" +type ClearLabelsFromLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was unlabeled. + """ + labelable: Labelable +} + +""" +Autogenerated input type of CloneProject +""" +input CloneProjectInput { + """ + The description of the project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not to clone the source project's workflows. + """ + includeWorkflows: Boolean! + + """ + The name of the project. + """ + name: String! + + """ + The visibility of the project, defaults to false (private). + """ + public: Boolean + + """ + The source project to clone. + """ + sourceId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The owner ID to create the project under. + """ + targetOwnerId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository", "User"], abstractType: "ProjectOwner") +} + +""" +Autogenerated return type of CloneProject +""" +type CloneProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the JobStatus for populating cloned fields. + """ + jobStatusId: String + + """ + The new cloned project. + """ + project: Project +} + +""" +Autogenerated input type of CloneTemplateRepository +""" +input CloneTemplateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A short description of the new repository. + """ + description: String + + """ + Whether to copy all branches from the template to the new repository. Defaults + to copying only the default branch of the template. + """ + includeAllBranches: Boolean = false + + """ + The name of the new repository. + """ + name: String! + + """ + The ID of the owner for the new repository. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "RepositoryOwner") + + """ + The Node ID of the template repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Autogenerated return type of CloneTemplateRepository +""" +type CloneTemplateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository. + """ + repository: Repository +} + +""" +An object that can be closed +""" +interface Closable { + """ + `true` if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime +} + +""" +Autogenerated input type of CloseIssue +""" +input CloseIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to be closed. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of CloseIssue +""" +type CloseIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was closed. + """ + issue: Issue +} + +""" +Autogenerated input type of ClosePullRequest +""" +input ClosePullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be closed. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ClosePullRequest +""" +type ClosePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was closed. + """ + pullRequest: PullRequest +} + +""" +Represents a 'closed' event on any `Closable`. +""" +type ClosedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Object that was closed. + """ + closable: Closable! + + """ + Object which triggered the creation of this event. + """ + closer: Closer + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The HTTP path for this closed event. + """ + resourcePath: URI! + + """ + The HTTP URL for this closed event. + """ + url: URI! +} + +""" +The object which triggered a `ClosedEvent`. +""" +union Closer = Commit | PullRequest + +""" +The Code of Conduct for a repository +""" +type CodeOfConduct implements Node { + """ + The body of the Code of Conduct + """ + body: String + id: ID! + + """ + The key for the Code of Conduct + """ + key: String! + + """ + The formal name of the Code of Conduct + """ + name: String! + + """ + The HTTP path for this Code of Conduct + """ + resourcePath: URI + + """ + The HTTP URL for this Code of Conduct + """ + url: URI +} + +""" +Collaborators affiliation level with a subject. +""" +enum CollaboratorAffiliation { + """ + All collaborators the authenticated user can see. + """ + ALL + + """ + All collaborators with permissions to an organization-owned subject, regardless of organization membership status. + """ + DIRECT + + """ + All outside collaborators of an organization-owned subject. + """ + OUTSIDE +} + +""" +Represents a comment. +""" +interface Comment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +A comment author association with repository. +""" +enum CommentAuthorAssociation { + """ + Author has been invited to collaborate on the repository. + """ + COLLABORATOR + + """ + Author has previously committed to the repository. + """ + CONTRIBUTOR + + """ + Author has not previously committed to GitHub. + """ + FIRST_TIMER + + """ + Author has not previously committed to the repository. + """ + FIRST_TIME_CONTRIBUTOR + + """ + Author is a placeholder for an unclaimed user. + """ + MANNEQUIN + + """ + Author is a member of the organization that owns the repository. + """ + MEMBER + + """ + Author has no association with the repository. + """ + NONE + + """ + Author is the owner of the repository. + """ + OWNER +} + +""" +The possible errors that will prevent a user from updating a comment. +""" +enum CommentCannotUpdateReason { + """ + Unable to create comment because repository is archived. + """ + ARCHIVED + + """ + You cannot update this comment + """ + DENIED + + """ + You must be the author or have write access to this repository to update this comment. + """ + INSUFFICIENT_ACCESS + + """ + Unable to create comment because issue is locked. + """ + LOCKED + + """ + You must be logged in to update this comment. + """ + LOGIN_REQUIRED + + """ + Repository is under maintenance. + """ + MAINTENANCE + + """ + At least one email address must be verified to update this comment. + """ + VERIFIED_EMAIL_REQUIRED +} + +""" +Represents a 'comment_deleted' event on a given issue or pull request. +""" +type CommentDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The user who authored the deleted comment. + """ + deletedCommentAuthor: Actor + id: ID! +} + +""" +Represents a Git commit. +""" +type Commit implements GitObject & Node & Subscribable & UniformResourceLocatable { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The number of additions in this commit. + """ + additions: Int! + + """ + The pull requests associated with a commit + """ + associatedPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests. + """ + orderBy: PullRequestOrder = {field: CREATED_AT, direction: ASC} + ): PullRequestConnection + + """ + Authorship details of the commit. + """ + author: GitActor + + """ + Check if the committer and the author match. + """ + authoredByCommitter: Boolean! + + """ + The datetime when this commit was authored. + """ + authoredDate: DateTime! + + """ + The list of authors for this commit based on the git author and the Co-authored-by + message trailer. The git author will always be first. + """ + authors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GitActorConnection! + + """ + Fetches `git blame` information. + """ + blame( + """ + The file whose Git blame information you want. + """ + path: String! + ): Blame! + + """ + The number of changed files in this commit. + """ + changedFiles: Int! + + """ + The check suites associated with a commit. + """ + checkSuites( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filters the check suites by this type. + """ + filterBy: CheckSuiteFilter + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CheckSuiteConnection + + """ + Comments made on the commit. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + The datetime when this commit was committed. + """ + committedDate: DateTime! + + """ + Check if committed via GitHub web UI. + """ + committedViaWeb: Boolean! + + """ + Committer details of the commit. + """ + committer: GitActor + + """ + The number of deletions in this commit. + """ + deletions: Int! + + """ + The deployments associated with a commit. + """ + deployments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Environments to list deployments for + """ + environments: [String!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for deployments returned from the connection. + """ + orderBy: DeploymentOrder = {field: CREATED_AT, direction: ASC} + ): DeploymentConnection + + """ + The tree entry representing the file located at the given path. + """ + file( + """ + The path for the file + """ + path: String! + ): TreeEntry + + """ + The linear commit history starting from (and including) this commit, in the same order as `git log`. + """ + history( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + If non-null, filters history to only show commits with matching authorship. + """ + author: CommitAuthor + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If non-null, filters history to only show commits touching files under this path. + """ + path: String + + """ + Allows specifying a beginning time or date for fetching commits. + """ + since: GitTimestamp + + """ + Allows specifying an ending time or date for fetching commits. + """ + until: GitTimestamp + ): CommitHistoryConnection! + id: ID! + + """ + The Git commit message + """ + message: String! + + """ + The Git commit message body + """ + messageBody: String! + + """ + The commit message body rendered to HTML. + """ + messageBodyHTML: HTML! + + """ + The Git commit message headline + """ + messageHeadline: String! + + """ + The commit message headline rendered to HTML. + """ + messageHeadlineHTML: HTML! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The organization this commit was made on behalf of. + """ + onBehalfOf: Organization + + """ + The parents of a commit. + """ + parents( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitConnection! + + """ + The datetime when this commit was pushed. + """ + pushedDate: DateTime + + """ + The Repository this commit belongs to + """ + repository: Repository! + + """ + The HTTP path for this commit + """ + resourcePath: URI! + + """ + Commit signing information, if present. + """ + signature: GitSignature + + """ + Status information for this commit + """ + status: Status + + """ + Check and Status rollup information for this commit. + """ + statusCheckRollup: StatusCheckRollup + + """ + Returns a list of all submodules in this repository as of this Commit parsed from the .gitmodules file. + """ + submodules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SubmoduleConnection! + + """ + Returns a URL to download a tarball archive for a repository. + Note: For private repositories, these links are temporary and expire after five minutes. + """ + tarballUrl: URI! + + """ + Commit's root Tree + """ + tree: Tree! + + """ + The HTTP path for the tree of this commit + """ + treeResourcePath: URI! + + """ + The HTTP URL for the tree of this commit + """ + treeUrl: URI! + + """ + The HTTP URL for this commit + """ + url: URI! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + Returns a URL to download a zipball archive for a repository. + Note: For private repositories, these links are temporary and expire after five minutes. + """ + zipballUrl: URI! +} + +""" +Specifies an author for filtering Git commits. +""" +input CommitAuthor { + """ + Email addresses to filter by. Commits authored by any of the specified email addresses will be returned. + """ + emails: [String!] + + """ + ID of a User to filter by. If non-null, only commits authored by this user + will be returned. This field takes precedence over emails. + """ + id: ID +} + +""" +Represents a comment on a given Commit. +""" +type CommitComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the comment body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the commit associated with the comment, if the commit exists. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. + """ + minimizedReason: String + + """ + Identifies the file path associated with the comment. + """ + path: String + + """ + Identifies the line position associated with the comment. + """ + position: Int + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this commit comment. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this commit comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for CommitComment. +""" +type CommitCommentConnection { + """ + A list of edges. + """ + edges: [CommitCommentEdge] + + """ + A list of nodes. + """ + nodes: [CommitComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CommitCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CommitComment +} + +""" +A thread of comments on a commit. +""" +type CommitCommentThread implements Node & RepositoryNode { + """ + The comments that exist in this thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The commit the comments were made on. + """ + commit: Commit + id: ID! + + """ + The file the comments were made on. + """ + path: String + + """ + The position in the diff for the commit that the comment was made on. + """ + position: Int + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +The connection type for Commit. +""" +type CommitConnection { + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Ordering options for commit contribution connections. +""" +input CommitContributionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field by which to order commit contributions. + """ + field: CommitContributionOrderField! +} + +""" +Properties by which commit contribution connections can be ordered. +""" +enum CommitContributionOrderField { + """ + Order commit contributions by how many commits they represent. + """ + COMMIT_COUNT + + """ + Order commit contributions by when they were made. + """ + OCCURRED_AT +} + +""" +This aggregates commits made by a user within one repository. +""" +type CommitContributionsByRepository { + """ + The commit contributions, each representing a day. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for commit contributions returned from the connection. + """ + orderBy: CommitContributionOrder = {field: OCCURRED_AT, direction: DESC} + ): CreatedCommitContributionConnection! + + """ + The repository in which the commits were made. + """ + repository: Repository! + + """ + The HTTP path for the user's commits to the repository in this time range. + """ + resourcePath: URI! + + """ + The HTTP URL for the user's commits to the repository in this time range. + """ + url: URI! +} + +""" +An edge in a connection. +""" +type CommitEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Commit +} + +""" +The connection type for Commit. +""" +type CommitHistoryConnection { + """ + A list of edges. + """ + edges: [CommitEdge] + + """ + A list of nodes. + """ + nodes: [Commit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a 'connected' event on a given issue or pull request. +""" +type ConnectedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Issue or pull request that made the reference. + """ + source: ReferencedSubject! + + """ + Issue or pull request which was connected. + """ + subject: ReferencedSubject! +} + +""" +A content attachment +""" +type ContentAttachment { + """ + The body text of the content attachment. This parameter supports markdown. + """ + body: String! + + """ + The content reference that the content attachment is attached to. + """ + contentReference: ContentReference! + + """ + Identifies the primary key from the database. + """ + databaseId: Int! + id: ID! + + """ + The title of the content attachment. + """ + title: String! +} + +""" +A content reference +""" +type ContentReference { + """ + Identifies the primary key from the database. + """ + databaseId: Int! + id: ID! + + """ + The reference of the content reference. + """ + reference: String! +} + +""" +Represents a contribution a user made on GitHub, such as opening an issue. +""" +interface Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A calendar of contributions made on GitHub by a user. +""" +type ContributionCalendar { + """ + A list of hex color codes used in this calendar. The darker the color, the more contributions it represents. + """ + colors: [String!]! + + """ + Determine if the color set was chosen because it's currently Halloween. + """ + isHalloween: Boolean! + + """ + A list of the months of contributions in this calendar. + """ + months: [ContributionCalendarMonth!]! + + """ + The count of total contributions in the calendar. + """ + totalContributions: Int! + + """ + A list of the weeks of contributions in this calendar. + """ + weeks: [ContributionCalendarWeek!]! +} + +""" +Represents a single day of contributions on GitHub by a user. +""" +type ContributionCalendarDay { + """ + The hex color code that represents how many contributions were made on this day compared to others in the calendar. + """ + color: String! + + """ + How many contributions were made by the user on this day. + """ + contributionCount: Int! + + """ + Indication of contributions, relative to other days. Can be used to indicate + which color to represent this day on a calendar. + """ + contributionLevel: ContributionLevel! + + """ + The day this square represents. + """ + date: Date! + + """ + A number representing which day of the week this square represents, e.g., 1 is Monday. + """ + weekday: Int! +} + +""" +A month of contributions in a user's contribution graph. +""" +type ContributionCalendarMonth { + """ + The date of the first day of this month. + """ + firstDay: Date! + + """ + The name of the month. + """ + name: String! + + """ + How many weeks started in this month. + """ + totalWeeks: Int! + + """ + The year the month occurred in. + """ + year: Int! +} + +""" +A week of contributions in a user's contribution graph. +""" +type ContributionCalendarWeek { + """ + The days of contributions in this week. + """ + contributionDays: [ContributionCalendarDay!]! + + """ + The date of the earliest square in this week. + """ + firstDay: Date! +} + +""" +Varying levels of contributions from none to many. +""" +enum ContributionLevel { + """ + Lowest 25% of days of contributions. + """ + FIRST_QUARTILE + + """ + Highest 25% of days of contributions. More contributions than the third quartile. + """ + FOURTH_QUARTILE + + """ + No contributions occurred. + """ + NONE + + """ + Second lowest 25% of days of contributions. More contributions than the first quartile. + """ + SECOND_QUARTILE + + """ + Second highest 25% of days of contributions. More contributions than second quartile, less than the fourth quartile. + """ + THIRD_QUARTILE +} + +""" +Ordering options for contribution connections. +""" +input ContributionOrder { + """ + The ordering direction. + """ + direction: OrderDirection! +} + +""" +A contributions collection aggregates contributions such as opened issues and commits created by a user. +""" +type ContributionsCollection { + """ + Commit contributions made by the user, grouped by repository. + """ + commitContributionsByRepository( + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [CommitContributionsByRepository!]! + + """ + A calendar of this user's contributions on GitHub. + """ + contributionCalendar: ContributionCalendar! + + """ + The years the user has been making contributions with the most recent year first. + """ + contributionYears: [Int!]! + + """ + Determine if this collection's time span ends in the current month. + """ + doesEndInCurrentMonth: Boolean! + + """ + The date of the first restricted contribution the user made in this time + period. Can only be non-null when the user has enabled private contribution counts. + """ + earliestRestrictedContributionDate: Date + + """ + The ending date and time of this collection. + """ + endedAt: DateTime! + + """ + The first issue the user opened on GitHub. This will be null if that issue was + opened outside the collection's time range and ignoreTimeRange is false. If + the issue is not visible but the user has opted to show private contributions, + a RestrictedContribution will be returned. + """ + firstIssueContribution: CreatedIssueOrRestrictedContribution + + """ + The first pull request the user opened on GitHub. This will be null if that + pull request was opened outside the collection's time range and + ignoreTimeRange is not true. If the pull request is not visible but the user + has opted to show private contributions, a RestrictedContribution will be returned. + """ + firstPullRequestContribution: CreatedPullRequestOrRestrictedContribution + + """ + The first repository the user created on GitHub. This will be null if that + first repository was created outside the collection's time range and + ignoreTimeRange is false. If the repository is not visible, then a + RestrictedContribution is returned. + """ + firstRepositoryContribution: CreatedRepositoryOrRestrictedContribution + + """ + Does the user have any more activity in the timeline that occurred prior to the collection's time range? + """ + hasActivityInThePast: Boolean! + + """ + Determine if there are any contributions in this collection. + """ + hasAnyContributions: Boolean! + + """ + Determine if the user made any contributions in this time frame whose details + are not visible because they were made in a private repository. Can only be + true if the user enabled private contribution counts. + """ + hasAnyRestrictedContributions: Boolean! + + """ + Whether or not the collector's time span is all within the same day. + """ + isSingleDay: Boolean! + + """ + A list of issues the user opened. + """ + issueContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first issue ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from the result. + """ + excludePopular: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedIssueContributionConnection! + + """ + Issue contributions made by the user, grouped by repository. + """ + issueContributionsByRepository( + """ + Should the user's first issue ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from the result. + """ + excludePopular: Boolean = false + + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [IssueContributionsByRepository!]! + + """ + When the user signed up for GitHub. This will be null if that sign up date + falls outside the collection's time range and ignoreTimeRange is false. + """ + joinedGitHubContribution: JoinedGitHubContribution + + """ + The date of the most recent restricted contribution the user made in this time + period. Can only be non-null when the user has enabled private contribution counts. + """ + latestRestrictedContributionDate: Date + + """ + When this collection's time range does not include any activity from the user, use this + to get a different collection from an earlier time range that does have activity. + """ + mostRecentCollectionWithActivity: ContributionsCollection + + """ + Returns a different contributions collection from an earlier time range than this one + that does not have any contributions. + """ + mostRecentCollectionWithoutActivity: ContributionsCollection + + """ + The issue the user opened on GitHub that received the most comments in the specified + time frame. + """ + popularIssueContribution: CreatedIssueContribution + + """ + The pull request the user opened on GitHub that received the most comments in the + specified time frame. + """ + popularPullRequestContribution: CreatedPullRequestContribution + + """ + Pull request contributions made by the user. + """ + pullRequestContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first pull request ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from the result. + """ + excludePopular: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestContributionConnection! + + """ + Pull request contributions made by the user, grouped by repository. + """ + pullRequestContributionsByRepository( + """ + Should the user's first pull request ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from the result. + """ + excludePopular: Boolean = false + + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [PullRequestContributionsByRepository!]! + + """ + Pull request review contributions made by the user. + """ + pullRequestReviewContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestReviewContributionConnection! + + """ + Pull request review contributions made by the user, grouped by repository. + """ + pullRequestReviewContributionsByRepository( + """ + How many repositories should be included. + """ + maxRepositories: Int = 25 + ): [PullRequestReviewContributionsByRepository!]! + + """ + A list of repositories owned by the user that the user created in this time range. + """ + repositoryContributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Should the user's first repository ever be excluded from the result. + """ + excludeFirst: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedRepositoryContributionConnection! + + """ + A count of contributions made by the user that the viewer cannot access. Only + non-zero when the user has chosen to share their private contribution counts. + """ + restrictedContributionsCount: Int! + + """ + The beginning date and time of this collection. + """ + startedAt: DateTime! + + """ + How many commits were made by the user in this time span. + """ + totalCommitContributions: Int! + + """ + How many issues the user opened. + """ + totalIssueContributions( + """ + Should the user's first issue ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many pull requests the user opened. + """ + totalPullRequestContributions( + """ + Should the user's first pull request ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many pull request reviews the user left. + """ + totalPullRequestReviewContributions: Int! + + """ + How many different repositories the user committed to. + """ + totalRepositoriesWithContributedCommits: Int! + + """ + How many different repositories the user opened issues in. + """ + totalRepositoriesWithContributedIssues( + """ + Should the user's first issue ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented issue be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many different repositories the user left pull request reviews in. + """ + totalRepositoriesWithContributedPullRequestReviews: Int! + + """ + How many different repositories the user opened pull requests in. + """ + totalRepositoriesWithContributedPullRequests( + """ + Should the user's first pull request ever be excluded from this count. + """ + excludeFirst: Boolean = false + + """ + Should the user's most commented pull request be excluded from this count. + """ + excludePopular: Boolean = false + ): Int! + + """ + How many repositories the user created. + """ + totalRepositoryContributions( + """ + Should the user's first repository ever be excluded from this count. + """ + excludeFirst: Boolean = false + ): Int! + + """ + The user who made the contributions in this collection. + """ + user: User! +} + +""" +Autogenerated input type of ConvertProjectCardNoteToIssue +""" +input ConvertProjectCardNoteToIssueInput { + """ + The body of the newly created issue. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ProjectCard ID to convert. + """ + projectCardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + The ID of the repository to create the issue in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the newly created issue. Defaults to the card's note text. + """ + title: String +} + +""" +Autogenerated return type of ConvertProjectCardNoteToIssue +""" +type ConvertProjectCardNoteToIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated ProjectCard. + """ + projectCard: ProjectCard +} + +""" +Represents a 'convert_to_draft' event on a given pull request. +""" +type ConvertToDraftEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this convert to draft event. + """ + resourcePath: URI! + + """ + The HTTP URL for this convert to draft event. + """ + url: URI! +} + +""" +Represents a 'converted_note_to_issue' event on a given issue or pull request. +""" +type ConvertedNoteToIssueEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard @preview(toggledBy: "starfox-preview") + + """ + Column name referenced by this project event. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +Autogenerated input type of CreateBranchProtectionRule +""" +input CreateBranchProtectionRuleInput { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean + + """ + Can admins overwrite branch protection. + """ + isAdminEnforced: Boolean + + """ + The glob-like pattern used to determine matching branches. + """ + pattern: String! + + """ + A list of User, Team or App IDs allowed to push to matching branches. + """ + pushActorIds: [ID!] + + """ + The global relay id of the repository in which a new branch protection rule should be created in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean + + """ + A list of User or Team IDs allowed to dismiss reviews on pull requests targeting matching branches. + """ + reviewDismissalActorIds: [ID!] +} + +""" +Autogenerated return type of CreateBranchProtectionRule +""" +type CreateBranchProtectionRulePayload { + """ + The newly created BranchProtectionRule. + """ + branchProtectionRule: BranchProtectionRule + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCheckRun +""" +input CreateCheckRunInput { + """ + Possible further actions the integrator can perform, which a user may trigger. + """ + actions: [CheckRunAction!] + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that the check run finished. + """ + completedAt: DateTime + + """ + The final conclusion of the check. + """ + conclusion: CheckConclusionState + + """ + The URL of the integrator's site that has the full details of the check. + """ + detailsUrl: URI + + """ + A reference for the run on the integrator's system. + """ + externalId: String + + """ + The SHA of the head commit. + """ + headSha: GitObjectID! + + """ + The name of the check. + """ + name: String! + + """ + Descriptive details about the run. + """ + output: CheckRunOutput + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The time that the check run began. + """ + startedAt: DateTime + + """ + The current status. + """ + status: RequestableCheckStatusState +} + +""" +Autogenerated return type of CreateCheckRun +""" +type CreateCheckRunPayload { + """ + The newly created check run. + """ + checkRun: CheckRun + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateCheckSuite +""" +input CreateCheckSuiteInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The SHA of the head commit. + """ + headSha: GitObjectID! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateCheckSuite +""" +type CreateCheckSuitePayload { + """ + The newly created check suite. + """ + checkSuite: CheckSuite + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of CreateContentAttachment +""" +input CreateContentAttachmentInput { + """ + The body of the content attachment, which may contain markdown. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The node ID of the content_reference. + """ + contentReferenceId: ID! @possibleTypes(concreteTypes: ["ContentReference"]) + + """ + The title of the content attachment. + """ + title: String! +} + +""" +Autogenerated return type of CreateContentAttachment +""" +type CreateContentAttachmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created content attachment. + """ + contentAttachment: ContentAttachment +} + +""" +Autogenerated input type of CreateDeployment +""" +input CreateDeploymentInput @preview(toggledBy: "flash-preview") { + """ + Attempt to automatically merge the default branch into the requested ref, defaults to true. + """ + autoMerge: Boolean = true + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Short description of the deployment. + """ + description: String = "" + + """ + Name for the target deployment environment. + """ + environment: String = "production" + + """ + JSON payload with extra information about the deployment. + """ + payload: String = "{}" + + """ + The node ID of the ref to be deployed. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The status contexts to verify against commit status checks. To bypass required + contexts, pass an empty array. Defaults to all unique contexts. + """ + requiredContexts: [String!] + + """ + Specifies a task to execute. + """ + task: String = "deploy" +} + +""" +Autogenerated return type of CreateDeployment +""" +type CreateDeploymentPayload @preview(toggledBy: "flash-preview") { + """ + True if the default branch has been auto-merged into the deployment ref. + """ + autoMerged: Boolean + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new deployment. + """ + deployment: Deployment +} + +""" +Autogenerated input type of CreateDeploymentStatus +""" +input CreateDeploymentStatusInput @preview(toggledBy: "flash-preview") { + """ + Adds a new inactive status to all non-transient, non-production environment + deployments with the same repository and environment name as the created + status's deployment. + """ + autoInactive: Boolean = true + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The node ID of the deployment. + """ + deploymentId: ID! @possibleTypes(concreteTypes: ["Deployment"]) + + """ + A short description of the status. Maximum length of 140 characters. + """ + description: String = "" + + """ + If provided, updates the environment of the deploy. Otherwise, does not modify the environment. + """ + environment: String + + """ + Sets the URL for accessing your environment. + """ + environmentUrl: String = "" + + """ + The log URL to associate with this status. This URL should contain + output to keep the user updated while the task is running or serve as + historical information for what happened in the deployment. + """ + logUrl: String = "" + + """ + The state of the deployment. + """ + state: DeploymentStatusState! +} + +""" +Autogenerated return type of CreateDeploymentStatus +""" +type CreateDeploymentStatusPayload @preview(toggledBy: "flash-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new deployment status. + """ + deploymentStatus: DeploymentStatus +} + +""" +Autogenerated input type of CreateEnterpriseOrganization +""" +input CreateEnterpriseOrganizationInput { + """ + The logins for the administrators of the new organization. + """ + adminLogins: [String!]! + + """ + The email used for sending billing receipts. + """ + billingEmail: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise owning the new organization. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the new organization. + """ + login: String! + + """ + The profile name of the new organization. + """ + profileName: String! +} + +""" +Autogenerated return type of CreateEnterpriseOrganization +""" +type CreateEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise that owns the created organization. + """ + enterprise: Enterprise + + """ + The organization that was created. + """ + organization: Organization +} + +""" +Autogenerated input type of CreateIpAllowListEntry +""" +input CreateIpAllowListEntryInput { + """ + An IP address or range of addresses in CIDR notation. + """ + allowListValue: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether the IP allow list entry is active when an IP allow list is enabled. + """ + isActive: Boolean! + + """ + An optional name for the IP allow list entry. + """ + name: String + + """ + The ID of the owner for which to create the new IP allow list entry. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "IpAllowListOwner") +} + +""" +Autogenerated return type of CreateIpAllowListEntry +""" +type CreateIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was created. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of CreateIssue +""" +input CreateIssueInput { + """ + The Node ID for the user assignee for this issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body for the issue description. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of an issue template in the repository, assigns labels and assignees from the template to the issue + """ + issueTemplate: String + + """ + An array of Node IDs of labels for this issue. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + The Node ID of the milestone for this issue. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this issue. + """ + projectIds: [ID!] @possibleTypes(concreteTypes: ["Project"]) + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title for the issue. + """ + title: String! +} + +""" +Autogenerated return type of CreateIssue +""" +type CreateIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new issue. + """ + issue: Issue +} + +""" +Autogenerated input type of CreateLabel +""" +input CreateLabelInput @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A 6 character hex code, without the leading #, identifying the color of the label. + """ + color: String! + + """ + A brief description of the label, such as its purpose. + """ + description: String + + """ + The name of the label. + """ + name: String! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateLabel +""" +type CreateLabelPayload @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new label. + """ + label: Label +} + +""" +Autogenerated input type of CreateProject +""" +input CreateProjectInput { + """ + The description of project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project. + """ + name: String! + + """ + The owner ID to create the project under. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Organization", "Repository", "User"], abstractType: "ProjectOwner") + + """ + A list of repository IDs to create as linked repositories for the project + """ + repositoryIds: [ID!] @possibleTypes(concreteTypes: ["Repository"]) + + """ + The name of the GitHub-provided template. + """ + template: ProjectTemplate +} + +""" +Autogenerated return type of CreateProject +""" +type CreateProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new project. + """ + project: Project +} + +""" +Autogenerated input type of CreatePullRequest +""" +input CreatePullRequestInput { + """ + The name of the branch you want your changes pulled into. This should be an existing branch + on the current repository. You cannot update the base branch on a pull request to point + to another repository. + """ + baseRefName: String! + + """ + The contents of the pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Indicates whether this pull request should be a draft. + """ + draft: Boolean = false + + """ + The name of the branch where your changes are implemented. For cross-repository pull requests + in the same network, namespace `head_ref_name` with a user like this: `username:branch`. + """ + headRefName: String! + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean = true + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The title of the pull request. + """ + title: String! +} + +""" +Autogenerated return type of CreatePullRequest +""" +type CreatePullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of CreateRef +""" +input CreateRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The fully qualified name of the new Ref (ie: `refs/heads/my_new_branch`). + """ + name: String! + + """ + The GitObjectID that the new Ref shall target. Must point to a commit. + """ + oid: GitObjectID! + + """ + The Node ID of the Repository to create the Ref in. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of CreateRef +""" +type CreateRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The newly created ref. + """ + ref: Ref +} + +""" +Autogenerated input type of CreateRepository +""" +input CreateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A short description of the new repository. + """ + description: String + + """ + Indicates if the repository should have the issues feature enabled. + """ + hasIssuesEnabled: Boolean = true + + """ + Indicates if the repository should have the wiki feature enabled. + """ + hasWikiEnabled: Boolean = false + + """ + The URL for a web page about this repository. + """ + homepageUrl: URI + + """ + The name of the new repository. + """ + name: String! + + """ + The ID of the owner for the new repository. + """ + ownerId: ID @possibleTypes(concreteTypes: ["Organization", "User"], abstractType: "RepositoryOwner") + + """ + When an organization is specified as the owner, this ID identifies the team + that should be granted access to the new repository. + """ + teamId: ID @possibleTypes(concreteTypes: ["Team"]) + + """ + Whether this repository should be marked as a template such that anyone who + can access it can create new repositories with the same files and directory structure. + """ + template: Boolean = false + + """ + Indicates the repository's visibility level. + """ + visibility: RepositoryVisibility! +} + +""" +Autogenerated return type of CreateRepository +""" +type CreateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new repository. + """ + repository: Repository +} + +""" +Autogenerated input type of CreateTeamDiscussionComment +""" +input CreateTeamDiscussionCommentInput { + """ + The content of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the discussion to which the comment belongs. + """ + discussionId: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) +} + +""" +Autogenerated return type of CreateTeamDiscussionComment +""" +type CreateTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new comment. + """ + teamDiscussionComment: TeamDiscussionComment +} + +""" +Autogenerated input type of CreateTeamDiscussion +""" +input CreateTeamDiscussionInput { + """ + The content of the discussion. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + If true, restricts the visibility of this discussion to team members and + organization admins. If false or not specified, allows any organization member + to view this discussion. + """ + private: Boolean + + """ + The ID of the team to which the discussion belongs. + """ + teamId: ID! @possibleTypes(concreteTypes: ["Team"]) + + """ + The title of the discussion. + """ + title: String! +} + +""" +Autogenerated return type of CreateTeamDiscussion +""" +type CreateTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new discussion. + """ + teamDiscussion: TeamDiscussion +} + +""" +Represents the contribution a user made by committing to a repository. +""" +type CreatedCommitContribution implements Contribution { + """ + How many commits were made on this day to this repository by the user. + """ + commitCount: Int! + + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The repository the user made a commit in. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedCommitContribution. +""" +type CreatedCommitContributionConnection { + """ + A list of edges. + """ + edges: [CreatedCommitContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedCommitContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of commits across days and repositories in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedCommitContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedCommitContribution +} + +""" +Represents the contribution a user made on GitHub by opening an issue. +""" +type CreatedIssueContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + The issue that was opened. + """ + issue: Issue! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedIssueContribution. +""" +type CreatedIssueContributionConnection { + """ + A list of edges. + """ + edges: [CreatedIssueContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedIssueContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedIssueContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedIssueContribution +} + +""" +Represents either a issue the viewer can access or a restricted contribution. +""" +union CreatedIssueOrRestrictedContribution = CreatedIssueContribution | RestrictedContribution + +""" +Represents the contribution a user made on GitHub by opening a pull request. +""" +type CreatedPullRequestContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The pull request that was opened. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedPullRequestContribution. +""" +type CreatedPullRequestContributionConnection { + """ + A list of edges. + """ + edges: [CreatedPullRequestContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedPullRequestContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedPullRequestContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedPullRequestContribution +} + +""" +Represents either a pull request the viewer can access or a restricted contribution. +""" +union CreatedPullRequestOrRestrictedContribution = CreatedPullRequestContribution | RestrictedContribution + +""" +Represents the contribution a user made by leaving a review on a pull request. +""" +type CreatedPullRequestReviewContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The pull request the user reviewed. + """ + pullRequest: PullRequest! + + """ + The review the user left on the pull request. + """ + pullRequestReview: PullRequestReview! + + """ + The repository containing the pull request that the user reviewed. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedPullRequestReviewContribution. +""" +type CreatedPullRequestReviewContributionConnection { + """ + A list of edges. + """ + edges: [CreatedPullRequestReviewContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedPullRequestReviewContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedPullRequestReviewContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedPullRequestReviewContribution +} + +""" +Represents the contribution a user made on GitHub by creating a repository. +""" +type CreatedRepositoryContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The repository that was created. + """ + repository: Repository! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +The connection type for CreatedRepositoryContribution. +""" +type CreatedRepositoryContributionConnection { + """ + A list of edges. + """ + edges: [CreatedRepositoryContributionEdge] + + """ + A list of nodes. + """ + nodes: [CreatedRepositoryContribution] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreatedRepositoryContributionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreatedRepositoryContribution +} + +""" +Represents either a repository the viewer can access or a restricted contribution. +""" +union CreatedRepositoryOrRestrictedContribution = CreatedRepositoryContribution | RestrictedContribution + +""" +Represents a mention made by one issue or pull request to another. +""" +type CrossReferencedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Identifies when the reference was made. + """ + referencedAt: DateTime! + + """ + The HTTP path for this pull request. + """ + resourcePath: URI! + + """ + Issue or pull request that made the reference. + """ + source: ReferencedSubject! + + """ + Issue or pull request to which the reference was made. + """ + target: ReferencedSubject! + + """ + The HTTP URL for this pull request. + """ + url: URI! + + """ + Checks if the target will be closed when the source is merged. + """ + willCloseTarget: Boolean! +} + +""" +An ISO-8601 encoded date string. +""" +scalar Date + +""" +An ISO-8601 encoded UTC date string. +""" +scalar DateTime + +""" +Autogenerated input type of DeclineTopicSuggestion +""" +input DeclineTopicSuggestionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the suggested topic. + """ + name: String! + + """ + The reason why the suggested topic is declined. + """ + reason: TopicSuggestionDeclineReason! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of DeclineTopicSuggestion +""" +type DeclineTopicSuggestionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The declined topic. + """ + topic: Topic +} + +""" +The possible default permissions for repositories. +""" +enum DefaultRepositoryPermissionField { + """ + Can read, write, and administrate repos by default + """ + ADMIN + + """ + No access + """ + NONE + + """ + Can read repos by default + """ + READ + + """ + Can read and write repos by default + """ + WRITE +} + +""" +Entities that can be deleted. +""" +interface Deletable { + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! +} + +""" +Autogenerated input type of DeleteBranchProtectionRule +""" +input DeleteBranchProtectionRuleInput { + """ + The global relay id of the branch protection rule to be deleted. + """ + branchProtectionRuleId: ID! @possibleTypes(concreteTypes: ["BranchProtectionRule"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of DeleteBranchProtectionRule +""" +type DeleteBranchProtectionRulePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteDeployment +""" +input DeleteDeploymentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the deployment to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Deployment"]) +} + +""" +Autogenerated return type of DeleteDeployment +""" +type DeleteDeploymentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteIpAllowListEntry +""" +input DeleteIpAllowListEntryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IP allow list entry to delete. + """ + ipAllowListEntryId: ID! @possibleTypes(concreteTypes: ["IpAllowListEntry"]) +} + +""" +Autogenerated return type of DeleteIpAllowListEntry +""" +type DeleteIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was deleted. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of DeleteIssueComment +""" +input DeleteIssueCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["IssueComment"]) +} + +""" +Autogenerated return type of DeleteIssueComment +""" +type DeleteIssueCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteIssue +""" +input DeleteIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to delete. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of DeleteIssue +""" +type DeleteIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository the issue belonged to + """ + repository: Repository +} + +""" +Autogenerated input type of DeleteLabel +""" +input DeleteLabelInput @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the label to be deleted. + """ + id: ID! @possibleTypes(concreteTypes: ["Label"]) +} + +""" +Autogenerated return type of DeleteLabel +""" +type DeleteLabelPayload @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeletePackageVersion +""" +input DeletePackageVersionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the package version to be deleted. + """ + packageVersionId: ID! @possibleTypes(concreteTypes: ["PackageVersion"]) +} + +""" +Autogenerated return type of DeletePackageVersion +""" +type DeletePackageVersionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not the operation succeeded. + """ + success: Boolean +} + +""" +Autogenerated input type of DeleteProjectCard +""" +input DeleteProjectCardInput { + """ + The id of the card to delete. + """ + cardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of DeleteProjectCard +""" +type DeleteProjectCardPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The column the deleted card was in. + """ + column: ProjectColumn + + """ + The deleted card ID. + """ + deletedCardId: ID +} + +""" +Autogenerated input type of DeleteProjectColumn +""" +input DeleteProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to delete. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of DeleteProjectColumn +""" +type DeleteProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted column ID. + """ + deletedColumnId: ID + + """ + The project the deleted column was in. + """ + project: Project +} + +""" +Autogenerated input type of DeleteProject +""" +input DeleteProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Project ID to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) +} + +""" +Autogenerated return type of DeleteProject +""" +type DeleteProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository or organization the project was removed from. + """ + owner: ProjectOwner +} + +""" +Autogenerated input type of DeletePullRequestReviewComment +""" +input DeletePullRequestReviewCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) +} + +""" +Autogenerated return type of DeletePullRequestReviewComment +""" +type DeletePullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request review the deleted comment belonged to. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of DeletePullRequestReview +""" +input DeletePullRequestReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request review to delete. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of DeletePullRequestReview +""" +type DeletePullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The deleted pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of DeleteRef +""" +input DeleteRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the Ref to be deleted. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) +} + +""" +Autogenerated return type of DeleteRef +""" +type DeleteRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteTeamDiscussionComment +""" +input DeleteTeamDiscussionCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussionComment"]) +} + +""" +Autogenerated return type of DeleteTeamDiscussionComment +""" +type DeleteTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteTeamDiscussion +""" +input DeleteTeamDiscussionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The discussion ID to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) +} + +""" +Autogenerated return type of DeleteTeamDiscussion +""" +type DeleteTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of DeleteVerifiableDomain +""" +input DeleteVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to delete. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of DeleteVerifiableDomain +""" +type DeleteVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The owning account from which the domain was deleted. + """ + owner: VerifiableDomainOwner +} + +""" +Represents a 'demilestoned' event on a given issue or pull request. +""" +type DemilestonedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the milestone title associated with the 'demilestoned' event. + """ + milestoneTitle: String! + + """ + Object referenced by event. + """ + subject: MilestoneItem! +} + +""" +A dependency manifest entry +""" +type DependencyGraphDependency @preview(toggledBy: "hawkgirl-preview") { + """ + Does the dependency itself have dependencies? + """ + hasDependencies: Boolean! + + """ + The dependency package manager + """ + packageManager: String + + """ + The required package name + """ + packageName: String! + + """ + The repository containing the package + """ + repository: Repository + + """ + The dependency version requirements + """ + requirements: String! +} + +""" +The connection type for DependencyGraphDependency. +""" +type DependencyGraphDependencyConnection @preview(toggledBy: "hawkgirl-preview") { + """ + A list of edges. + """ + edges: [DependencyGraphDependencyEdge] + + """ + A list of nodes. + """ + nodes: [DependencyGraphDependency] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DependencyGraphDependencyEdge @preview(toggledBy: "hawkgirl-preview") { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DependencyGraphDependency +} + +""" +Dependency manifest for a repository +""" +type DependencyGraphManifest implements Node @preview(toggledBy: "hawkgirl-preview") { + """ + Path to view the manifest file blob + """ + blobPath: String! + + """ + A list of manifest dependencies + """ + dependencies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DependencyGraphDependencyConnection + + """ + The number of dependencies listed in the manifest + """ + dependenciesCount: Int + + """ + Is the manifest too big to parse? + """ + exceedsMaxSize: Boolean! + + """ + Fully qualified manifest filename + """ + filename: String! + id: ID! + + """ + Were we able to parse the manifest? + """ + parseable: Boolean! + + """ + The repository containing the manifest + """ + repository: Repository! +} + +""" +The connection type for DependencyGraphManifest. +""" +type DependencyGraphManifestConnection @preview(toggledBy: "hawkgirl-preview") { + """ + A list of edges. + """ + edges: [DependencyGraphManifestEdge] + + """ + A list of nodes. + """ + nodes: [DependencyGraphManifest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DependencyGraphManifestEdge @preview(toggledBy: "hawkgirl-preview") { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DependencyGraphManifest +} + +""" +A repository deploy key. +""" +type DeployKey implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The deploy key. + """ + key: String! + + """ + Whether or not the deploy key is read only. + """ + readOnly: Boolean! + + """ + The deploy key title. + """ + title: String! + + """ + Whether or not the deploy key has been verified. + """ + verified: Boolean! +} + +""" +The connection type for DeployKey. +""" +type DeployKeyConnection { + """ + A list of edges. + """ + edges: [DeployKeyEdge] + + """ + A list of nodes. + """ + nodes: [DeployKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeployKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeployKey +} + +""" +Represents a 'deployed' event on a given pull request. +""" +type DeployedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The deployment associated with the 'deployed' event. + """ + deployment: Deployment! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The ref associated with the 'deployed' event. + """ + ref: Ref +} + +""" +Represents triggered deployment instance. +""" +type Deployment implements Node { + """ + Identifies the commit sha of the deployment. + """ + commit: Commit + + """ + Identifies the oid of the deployment commit, even if the commit has been deleted. + """ + commitOid: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who triggered the deployment. + """ + creator: Actor! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The deployment description. + """ + description: String + + """ + The latest environment to which this deployment was made. + """ + environment: String + id: ID! + + """ + The latest environment to which this deployment was made. + """ + latestEnvironment: String + + """ + The latest status of this deployment. + """ + latestStatus: DeploymentStatus + + """ + The original environment to which this deployment was made. + """ + originalEnvironment: String + + """ + Extra information that a deployment system might need. + """ + payload: String + + """ + Identifies the Ref of the deployment, if the deployment was created by ref. + """ + ref: Ref + + """ + Identifies the repository associated with the deployment. + """ + repository: Repository! + + """ + The current state of the deployment. + """ + state: DeploymentState + + """ + A list of statuses associated with the deployment. + """ + statuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeploymentStatusConnection + + """ + The deployment task. + """ + task: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for Deployment. +""" +type DeploymentConnection { + """ + A list of edges. + """ + edges: [DeploymentEdge] + + """ + A list of nodes. + """ + nodes: [Deployment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Deployment +} + +""" +Represents a 'deployment_environment_changed' event on a given pull request. +""" +type DeploymentEnvironmentChangedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The deployment status that updated the deployment environment. + """ + deploymentStatus: DeploymentStatus! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Ordering options for deployment connections +""" +input DeploymentOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order deployments by. + """ + field: DeploymentOrderField! +} + +""" +Properties by which deployment connections can be ordered. +""" +enum DeploymentOrderField { + """ + Order collection by creation time + """ + CREATED_AT +} + +""" +The possible states in which a deployment can be. +""" +enum DeploymentState { + """ + The pending deployment was not updated after 30 minutes. + """ + ABANDONED + + """ + The deployment is currently active. + """ + ACTIVE + + """ + An inactive transient deployment. + """ + DESTROYED + + """ + The deployment experienced an error. + """ + ERROR + + """ + The deployment has failed. + """ + FAILURE + + """ + The deployment is inactive. + """ + INACTIVE + + """ + The deployment is in progress. + """ + IN_PROGRESS + + """ + The deployment is pending. + """ + PENDING + + """ + The deployment has queued + """ + QUEUED + + """ + The deployment is waiting. + """ + WAITING +} + +""" +Describes the status of a given deployment attempt. +""" +type DeploymentStatus implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who triggered the deployment. + """ + creator: Actor! + + """ + Identifies the deployment associated with status. + """ + deployment: Deployment! + + """ + Identifies the description of the deployment. + """ + description: String + + """ + Identifies the environment of the deployment at the time of this deployment status + """ + environment: String @preview(toggledBy: "flash-preview") + + """ + Identifies the environment URL of the deployment. + """ + environmentUrl: URI + id: ID! + + """ + Identifies the log URL of the deployment. + """ + logUrl: URI + + """ + Identifies the current state of the deployment. + """ + state: DeploymentStatusState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for DeploymentStatus. +""" +type DeploymentStatusConnection { + """ + A list of edges. + """ + edges: [DeploymentStatusEdge] + + """ + A list of nodes. + """ + nodes: [DeploymentStatus] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DeploymentStatusEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DeploymentStatus +} + +""" +The possible states for a deployment status. +""" +enum DeploymentStatusState { + """ + The deployment experienced an error. + """ + ERROR + + """ + The deployment has failed. + """ + FAILURE + + """ + The deployment is inactive. + """ + INACTIVE + + """ + The deployment is in progress. + """ + IN_PROGRESS + + """ + The deployment is pending. + """ + PENDING + + """ + The deployment is queued + """ + QUEUED + + """ + The deployment was successful. + """ + SUCCESS + + """ + The deployment is waiting. + """ + WAITING +} + +""" +The possible sides of a diff. +""" +enum DiffSide { + """ + The left side of the diff. + """ + LEFT + + """ + The right side of the diff. + """ + RIGHT +} + +""" +Represents a 'disconnected' event on a given issue or pull request. +""" +type DisconnectedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Issue or pull request from which the issue was disconnected. + """ + source: ReferencedSubject! + + """ + Issue or pull request which was disconnected. + """ + subject: ReferencedSubject! +} + +""" +Autogenerated input type of DismissPullRequestReview +""" +input DismissPullRequestReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The contents of the pull request review dismissal message. + """ + message: String! + + """ + The Node ID of the pull request review to modify. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of DismissPullRequestReview +""" +type DismissPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The dismissed pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Specifies a review comment to be left with a Pull Request Review. +""" +input DraftPullRequestReviewComment { + """ + Body of the comment to leave. + """ + body: String! + + """ + Path to the file being commented on. + """ + path: String! + + """ + Position in the file to leave a comment on. + """ + position: Int! +} + +""" +Specifies a review comment thread to be left with a Pull Request Review. +""" +input DraftPullRequestReviewThread { + """ + Body of the comment to leave. + """ + body: String! + + """ + The line of the blob to which the thread refers. The end of the line range for multi-line comments. + """ + line: Int! + + """ + Path to the file being commented on. + """ + path: String! + + """ + The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. + """ + side: DiffSide = RIGHT + + """ + The first line of the range to which the comment refers. + """ + startLine: Int + + """ + The side of the diff on which the start line resides. + """ + startSide: DiffSide = RIGHT +} + +""" +An account to manage multiple organizations with consolidated policy and billing. +""" +type Enterprise implements Node { + """ + A URL pointing to the enterprise's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Enterprise billing information visible to enterprise billing managers. + """ + billingInfo: EnterpriseBillingInfo + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the enterprise. + """ + description: String + + """ + The description of the enterprise as HTML. + """ + descriptionHTML: HTML! + id: ID! + + """ + The location of the enterprise. + """ + location: String + + """ + A list of users who are members of this enterprise. + """ + members( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Only return members within the selected GitHub Enterprise deployment + """ + deployment: EnterpriseUserDeployment + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for members returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + Only return members within the organizations with these logins + """ + organizationLogins: [String!] + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the enterprise organization or server. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseMemberConnection! + + """ + The name of the enterprise. + """ + name: String! + + """ + A list of organizations that belong to this enterprise. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations returned from the connection. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + ): OrganizationConnection! + + """ + Enterprise information only visible to enterprise owners. + """ + ownerInfo: EnterpriseOwnerInfo + + """ + The HTTP path for this enterprise. + """ + resourcePath: URI! + + """ + The URL-friendly identifier for the enterprise. + """ + slug: String! + + """ + The HTTP URL for this enterprise. + """ + url: URI! + + """ + A list of user accounts on this enterprise. + """ + userAccounts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): EnterpriseUserAccountConnection! + + """ + Is the current viewer an admin of this enterprise? + """ + viewerIsAdmin: Boolean! + + """ + The URL of the enterprise website. + """ + websiteUrl: URI +} + +""" +The connection type for User. +""" +type EnterpriseAdministratorConnection { + """ + A list of edges. + """ + edges: [EnterpriseAdministratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is an administrator of an enterprise. +""" +type EnterpriseAdministratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User + + """ + The role of the administrator. + """ + role: EnterpriseAdministratorRole! +} + +""" +An invitation for a user to become an owner or billing manager of an enterprise. +""" +type EnterpriseAdministratorInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email of the person who was invited to the enterprise. + """ + email: String + + """ + The enterprise the invitation is for. + """ + enterprise: Enterprise! + id: ID! + + """ + The user who was invited to the enterprise. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User + + """ + The invitee's pending role in the enterprise (owner or billing_manager). + """ + role: EnterpriseAdministratorRole! +} + +""" +The connection type for EnterpriseAdministratorInvitation. +""" +type EnterpriseAdministratorInvitationConnection { + """ + A list of edges. + """ + edges: [EnterpriseAdministratorInvitationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseAdministratorInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseAdministratorInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseAdministratorInvitation +} + +""" +Ordering options for enterprise administrator invitation connections +""" +input EnterpriseAdministratorInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise administrator invitations by. + """ + field: EnterpriseAdministratorInvitationOrderField! +} + +""" +Properties by which enterprise administrator invitation connections can be ordered. +""" +enum EnterpriseAdministratorInvitationOrderField { + """ + Order enterprise administrator member invitations by creation time + """ + CREATED_AT +} + +""" +The possible administrator roles in an enterprise account. +""" +enum EnterpriseAdministratorRole { + """ + Represents a billing manager of the enterprise account. + """ + BILLING_MANAGER + + """ + Represents an owner of the enterprise account. + """ + OWNER +} + +""" +Metadata for an audit entry containing enterprise account information. +""" +interface EnterpriseAuditEntryData { + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI +} + +""" +Enterprise billing information visible to enterprise billing managers and owners. +""" +type EnterpriseBillingInfo { + """ + The number of licenseable users/emails across the enterprise. + """ + allLicensableUsersCount: Int! + + """ + The number of data packs used by all organizations owned by the enterprise. + """ + assetPacks: Int! + + """ + The number of available seats across all owned organizations based on the unique number of billable users. + """ + availableSeats: Int! @deprecated(reason: "`availableSeats` will be replaced with `totalAvailableLicenses` to provide more clarity on the value being returned Use EnterpriseBillingInfo.totalAvailableLicenses instead. Removal on 2020-01-01 UTC.") + + """ + The bandwidth quota in GB for all organizations owned by the enterprise. + """ + bandwidthQuota: Float! + + """ + The bandwidth usage in GB for all organizations owned by the enterprise. + """ + bandwidthUsage: Float! + + """ + The bandwidth usage as a percentage of the bandwidth quota. + """ + bandwidthUsagePercentage: Int! + + """ + The total seats across all organizations owned by the enterprise. + """ + seats: Int! @deprecated(reason: "`seats` will be replaced with `totalLicenses` to provide more clarity on the value being returned Use EnterpriseBillingInfo.totalLicenses instead. Removal on 2020-01-01 UTC.") + + """ + The storage quota in GB for all organizations owned by the enterprise. + """ + storageQuota: Float! + + """ + The storage usage in GB for all organizations owned by the enterprise. + """ + storageUsage: Float! + + """ + The storage usage as a percentage of the storage quota. + """ + storageUsagePercentage: Int! + + """ + The number of available licenses across all owned organizations based on the unique number of billable users. + """ + totalAvailableLicenses: Int! + + """ + The total number of licenses allocated. + """ + totalLicenses: Int! +} + +""" +The possible values for the enterprise default repository permission setting. +""" +enum EnterpriseDefaultRepositoryPermissionSettingValue { + """ + Organization members will be able to clone, pull, push, and add new collaborators to all organization repositories. + """ + ADMIN + + """ + Organization members will only be able to clone and pull public repositories. + """ + NONE + + """ + Organizations in the enterprise choose default repository permissions for their members. + """ + NO_POLICY + + """ + Organization members will be able to clone and pull all organization repositories. + """ + READ + + """ + Organization members will be able to clone, pull, and push all organization repositories. + """ + WRITE +} + +""" +The possible values for an enabled/disabled enterprise setting. +""" +enum EnterpriseEnabledDisabledSettingValue { + """ + The setting is disabled for organizations in the enterprise. + """ + DISABLED + + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED + + """ + There is no policy set for organizations in the enterprise. + """ + NO_POLICY +} + +""" +The possible values for an enabled/no policy enterprise setting. +""" +enum EnterpriseEnabledSettingValue { + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED + + """ + There is no policy set for organizations in the enterprise. + """ + NO_POLICY +} + +""" +An identity provider configured to provision identities for an enterprise. +""" +type EnterpriseIdentityProvider implements Node { + """ + The digest algorithm used to sign SAML requests for the identity provider. + """ + digestMethod: SamlDigestAlgorithm + + """ + The enterprise this identity provider belongs to. + """ + enterprise: Enterprise + + """ + ExternalIdentities provisioned by this identity provider. + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ExternalIdentityConnection! + id: ID! + + """ + The x509 certificate used by the identity provider to sign assertions and responses. + """ + idpCertificate: X509Certificate + + """ + The Issuer Entity ID for the SAML identity provider. + """ + issuer: String + + """ + Recovery codes that can be used by admins to access the enterprise if the identity provider is unavailable. + """ + recoveryCodes: [String!] + + """ + The signature algorithm used to sign SAML requests for the identity provider. + """ + signatureMethod: SamlSignatureAlgorithm + + """ + The URL endpoint for the identity provider's SAML SSO. + """ + ssoUrl: URI +} + +""" +An object that is a member of an enterprise. +""" +union EnterpriseMember = EnterpriseUserAccount | User + +""" +The connection type for EnterpriseMember. +""" +type EnterpriseMemberConnection { + """ + A list of edges. + """ + edges: [EnterpriseMemberEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseMember] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is a member of an enterprise through one or more organizations. +""" +type EnterpriseMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the user does not have a license for the enterprise. + """ + isUnlicensed: Boolean! @deprecated(reason: "All members consume a license Removal on 2021-01-01 UTC.") + + """ + The item at the end of the edge. + """ + node: EnterpriseMember +} + +""" +Ordering options for enterprise member connections. +""" +input EnterpriseMemberOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order enterprise members by. + """ + field: EnterpriseMemberOrderField! +} + +""" +Properties by which enterprise member connections can be ordered. +""" +enum EnterpriseMemberOrderField { + """ + Order enterprise members by creation time + """ + CREATED_AT + + """ + Order enterprise members by login + """ + LOGIN +} + +""" +The possible values for the enterprise members can create repositories setting. +""" +enum EnterpriseMembersCanCreateRepositoriesSettingValue { + """ + Members will be able to create public and private repositories. + """ + ALL + + """ + Members will not be able to create public or private repositories. + """ + DISABLED + + """ + Organization administrators choose whether to allow members to create repositories. + """ + NO_POLICY + + """ + Members will be able to create only private repositories. + """ + PRIVATE + + """ + Members will be able to create only public repositories. + """ + PUBLIC +} + +""" +The possible values for the members can make purchases setting. +""" +enum EnterpriseMembersCanMakePurchasesSettingValue { + """ + The setting is disabled for organizations in the enterprise. + """ + DISABLED + + """ + The setting is enabled for organizations in the enterprise. + """ + ENABLED +} + +""" +The connection type for Organization. +""" +type EnterpriseOrganizationMembershipConnection { + """ + A list of edges. + """ + edges: [EnterpriseOrganizationMembershipEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An enterprise organization that a user is a member of. +""" +type EnterpriseOrganizationMembershipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization + + """ + The role of the user in the enterprise membership. + """ + role: EnterpriseUserAccountMembershipRole! +} + +""" +The connection type for User. +""" +type EnterpriseOutsideCollaboratorConnection { + """ + A list of edges. + """ + edges: [EnterpriseOutsideCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A User who is an outside collaborator of an enterprise through one or more organizations. +""" +type EnterpriseOutsideCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the outside collaborator does not have a license for the enterprise. + """ + isUnlicensed: Boolean! @deprecated(reason: "All outside collaborators consume a license Removal on 2021-01-01 UTC.") + + """ + The item at the end of the edge. + """ + node: User + + """ + The enterprise organization repositories this user is a member of. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories. + """ + orderBy: RepositoryOrder = {field: NAME, direction: ASC} + ): EnterpriseRepositoryInfoConnection! +} + +""" +Enterprise information only visible to enterprise owners. +""" +type EnterpriseOwnerInfo { + """ + A list of all of the administrators for this enterprise. + """ + admins( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for administrators returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The role to filter by. + """ + role: EnterpriseAdministratorRole + ): EnterpriseAdministratorConnection! + + """ + A list of users in the enterprise who currently have two-factor authentication disabled. + """ + affiliatedUsersWithTwoFactorDisabled( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + Whether or not affiliated users with two-factor authentication disabled exist in the enterprise. + """ + affiliatedUsersWithTwoFactorDisabledExist: Boolean! + + """ + The setting value for whether private repository forking is enabled for repositories in organizations in this enterprise. + """ + allowPrivateRepositoryForkingSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided private repository forking setting value. + """ + allowPrivateRepositoryForkingSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for base repository permissions for organizations in this enterprise. + """ + defaultRepositoryPermissionSetting: EnterpriseDefaultRepositoryPermissionSettingValue! + + """ + A list of enterprise organizations configured with the provided default repository permission. + """ + defaultRepositoryPermissionSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The permission to find organizations for. + """ + value: DefaultRepositoryPermissionField! + ): OrganizationConnection! + + """ + A list of domains owned by the enterprise. + """ + domains( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter whether or not the domain is verified. + """ + isVerified: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for verifiable domains returned. + """ + orderBy: VerifiableDomainOrder = {field: DOMAIN, direction: ASC} + ): VerifiableDomainConnection! + + """ + Enterprise Server installations owned by the enterprise. + """ + enterpriseServerInstallations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Whether or not to only return installations discovered via GitHub Connect. + """ + connectedOnly: Boolean = false + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server installations returned. + """ + orderBy: EnterpriseServerInstallationOrder = {field: HOST_NAME, direction: ASC} + ): EnterpriseServerInstallationConnection! + + """ + The setting value for whether the enterprise has an IP allow list enabled. + """ + ipAllowListEnabledSetting: IpAllowListEnabledSettingValue! + + """ + The IP addresses that are allowed to access resources owned by the enterprise. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + Whether or not the default repository permission is currently being updated. + """ + isUpdatingDefaultRepositoryPermission: Boolean! + + """ + Whether the two-factor authentication requirement is currently being enforced. + """ + isUpdatingTwoFactorRequirement: Boolean! + + """ + The setting value for whether organization members with admin permissions on a + repository can change repository visibility. + """ + membersCanChangeRepositoryVisibilitySetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided can change repository visibility setting value. + """ + membersCanChangeRepositoryVisibilitySettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members of organizations in the enterprise can create internal repositories. + """ + membersCanCreateInternalRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create private repositories. + """ + membersCanCreatePrivateRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create public repositories. + """ + membersCanCreatePublicRepositoriesSetting: Boolean + + """ + The setting value for whether members of organizations in the enterprise can create repositories. + """ + membersCanCreateRepositoriesSetting: EnterpriseMembersCanCreateRepositoriesSettingValue + + """ + A list of enterprise organizations configured with the provided repository creation setting value. + """ + membersCanCreateRepositoriesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting to find organizations for. + """ + value: OrganizationMembersCanCreateRepositoriesSettingValue! + ): OrganizationConnection! + + """ + The setting value for whether members with admin permissions for repositories can delete issues. + """ + membersCanDeleteIssuesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can delete issues setting value. + """ + membersCanDeleteIssuesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members with admin permissions for repositories can delete or transfer repositories. + """ + membersCanDeleteRepositoriesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can delete repositories setting value. + """ + membersCanDeleteRepositoriesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members of organizations in the enterprise can invite outside collaborators. + """ + membersCanInviteCollaboratorsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can invite collaborators setting value. + """ + membersCanInviteCollaboratorsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + Indicates whether members of this enterprise's organizations can purchase additional services for those organizations. + """ + membersCanMakePurchasesSetting: EnterpriseMembersCanMakePurchasesSettingValue! + + """ + The setting value for whether members with admin permissions for repositories can update protected branches. + """ + membersCanUpdateProtectedBranchesSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can update protected branches setting value. + """ + membersCanUpdateProtectedBranchesSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether members can view dependency insights. + """ + membersCanViewDependencyInsightsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided members can view dependency insights setting value. + """ + membersCanViewDependencyInsightsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether organization projects are enabled for organizations in this enterprise. + """ + organizationProjectsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided organization projects setting value. + """ + organizationProjectsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + A list of outside collaborators across the repositories in the enterprise. + """ + outsideCollaborators( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The login of one specific outside collaborator. + """ + login: String + + """ + Ordering options for outside collaborators returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + Only return outside collaborators on repositories with this visibility. + """ + visibility: RepositoryVisibility + ): EnterpriseOutsideCollaboratorConnection! + + """ + A list of pending administrator invitations for the enterprise. + """ + pendingAdminInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending enterprise administrator invitations returned from the connection. + """ + orderBy: EnterpriseAdministratorInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + + """ + The role to filter by. + """ + role: EnterpriseAdministratorRole + ): EnterpriseAdministratorInvitationConnection! + + """ + A list of pending collaborator invitations across the repositories in the enterprise. + """ + pendingCollaboratorInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending repository collaborator invitations returned from the connection. + """ + orderBy: RepositoryInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + ): RepositoryInvitationConnection! + + """ + A list of pending collaborators across the repositories in the enterprise. + """ + pendingCollaborators( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pending repository collaborator invitations returned from the connection. + """ + orderBy: RepositoryInvitationOrder = {field: CREATED_AT, direction: DESC} + + """ + The search string to look for. + """ + query: String + ): EnterprisePendingCollaboratorConnection! @deprecated(reason: "Repository invitations can now be associated with an email, not only an invitee. Use the `pendingCollaboratorInvitations` field instead. Removal on 2020-10-01 UTC.") + + """ + A list of pending member invitations for organizations in the enterprise. + """ + pendingMemberInvitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The search string to look for. + """ + query: String + ): EnterprisePendingMemberInvitationConnection! + + """ + The setting value for whether repository projects are enabled in this enterprise. + """ + repositoryProjectsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided repository projects setting value. + """ + repositoryProjectsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The SAML Identity Provider for the enterprise. + """ + samlIdentityProvider: EnterpriseIdentityProvider + + """ + A list of enterprise organizations configured with the SAML single sign-on setting value. + """ + samlIdentityProviderSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: IdentityProviderConfigurationState! + ): OrganizationConnection! + + """ + A list of members with a support entitlement. + """ + supportEntitlements( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for support entitlement users returned from the connection. + """ + orderBy: EnterpriseMemberOrder = {field: LOGIN, direction: ASC} + ): EnterpriseMemberConnection! + + """ + The setting value for whether team discussions are enabled for organizations in this enterprise. + """ + teamDiscussionsSetting: EnterpriseEnabledDisabledSettingValue! + + """ + A list of enterprise organizations configured with the provided team discussions setting value. + """ + teamDiscussionsSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! + + """ + The setting value for whether the enterprise requires two-factor authentication for its organizations and users. + """ + twoFactorRequiredSetting: EnterpriseEnabledSettingValue! + + """ + A list of enterprise organizations configured with the two-factor authentication setting value. + """ + twoFactorRequiredSettingOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations with this setting. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The setting value to find organizations for. + """ + value: Boolean! + ): OrganizationConnection! +} + +""" +The connection type for User. +""" +type EnterprisePendingCollaboratorConnection { + """ + A list of edges. + """ + edges: [EnterprisePendingCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A user with an invitation to be a collaborator on a repository owned by an organization in an enterprise. +""" +type EnterprisePendingCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the invited collaborator does not have a license for the enterprise. + """ + isUnlicensed: Boolean! @deprecated(reason: "All pending collaborators consume a license Removal on 2021-01-01 UTC.") + + """ + The item at the end of the edge. + """ + node: User + + """ + The enterprise organization repositories this user is a member of. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories. + """ + orderBy: RepositoryOrder = {field: NAME, direction: ASC} + ): EnterpriseRepositoryInfoConnection! +} + +""" +The connection type for OrganizationInvitation. +""" +type EnterprisePendingMemberInvitationConnection { + """ + A list of edges. + """ + edges: [EnterprisePendingMemberInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the total count of unique users in the connection. + """ + totalUniqueUserCount: Int! +} + +""" +An invitation to be a member in an enterprise organization. +""" +type EnterprisePendingMemberInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the invitation has a license for the enterprise. + """ + isUnlicensed: Boolean! @deprecated(reason: "All pending members consume a license Removal on 2020-07-01 UTC.") + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +A subset of repository information queryable from an enterprise. +""" +type EnterpriseRepositoryInfo implements Node { + id: ID! + + """ + Identifies if the repository is private. + """ + isPrivate: Boolean! + + """ + The repository's name. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! +} + +""" +The connection type for EnterpriseRepositoryInfo. +""" +type EnterpriseRepositoryInfoConnection { + """ + A list of edges. + """ + edges: [EnterpriseRepositoryInfoEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseRepositoryInfo] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseRepositoryInfoEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseRepositoryInfo +} + +""" +An Enterprise Server installation. +""" +type EnterpriseServerInstallation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The customer name to which the Enterprise Server installation belongs. + """ + customerName: String! + + """ + The host name of the Enterprise Server installation. + """ + hostName: String! + id: ID! + + """ + Whether or not the installation is connected to an Enterprise Server installation via GitHub Connect. + """ + isConnected: Boolean! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + User accounts on this Enterprise Server installation. + """ + userAccounts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user accounts returned from the connection. + """ + orderBy: EnterpriseServerUserAccountOrder = {field: LOGIN, direction: ASC} + ): EnterpriseServerUserAccountConnection! + + """ + User accounts uploads for the Enterprise Server installation. + """ + userAccountsUploads( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user accounts uploads returned from the connection. + """ + orderBy: EnterpriseServerUserAccountsUploadOrder = {field: CREATED_AT, direction: DESC} + ): EnterpriseServerUserAccountsUploadConnection! +} + +""" +The connection type for EnterpriseServerInstallation. +""" +type EnterpriseServerInstallationConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerInstallationEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerInstallation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerInstallationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerInstallation +} + +""" +Ordering options for Enterprise Server installation connections. +""" +input EnterpriseServerInstallationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order Enterprise Server installations by. + """ + field: EnterpriseServerInstallationOrderField! +} + +""" +Properties by which Enterprise Server installation connections can be ordered. +""" +enum EnterpriseServerInstallationOrderField { + """ + Order Enterprise Server installations by creation time + """ + CREATED_AT + + """ + Order Enterprise Server installations by customer name + """ + CUSTOMER_NAME + + """ + Order Enterprise Server installations by host name + """ + HOST_NAME +} + +""" +A user account on an Enterprise Server installation. +""" +type EnterpriseServerUserAccount implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + User emails belonging to this user account. + """ + emails( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Enterprise Server user account emails returned from the connection. + """ + orderBy: EnterpriseServerUserAccountEmailOrder = {field: EMAIL, direction: ASC} + ): EnterpriseServerUserAccountEmailConnection! + + """ + The Enterprise Server installation on which this user account exists. + """ + enterpriseServerInstallation: EnterpriseServerInstallation! + id: ID! + + """ + Whether the user account is a site administrator on the Enterprise Server installation. + """ + isSiteAdmin: Boolean! + + """ + The login of the user account on the Enterprise Server installation. + """ + login: String! + + """ + The profile name of the user account on the Enterprise Server installation. + """ + profileName: String + + """ + The date and time when the user account was created on the Enterprise Server installation. + """ + remoteCreatedAt: DateTime! + + """ + The ID of the user account on the Enterprise Server installation. + """ + remoteUserId: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for EnterpriseServerUserAccount. +""" +type EnterpriseServerUserAccountConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccount] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccount +} + +""" +An email belonging to a user account on an Enterprise Server installation. +""" +type EnterpriseServerUserAccountEmail implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email address. + """ + email: String! + id: ID! + + """ + Indicates whether this is the primary email of the associated user account. + """ + isPrimary: Boolean! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user account to which the email belongs. + """ + userAccount: EnterpriseServerUserAccount! +} + +""" +The connection type for EnterpriseServerUserAccountEmail. +""" +type EnterpriseServerUserAccountEmailConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountEmailEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccountEmail] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountEmailEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccountEmail +} + +""" +Ordering options for Enterprise Server user account email connections. +""" +input EnterpriseServerUserAccountEmailOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order emails by. + """ + field: EnterpriseServerUserAccountEmailOrderField! +} + +""" +Properties by which Enterprise Server user account email connections can be ordered. +""" +enum EnterpriseServerUserAccountEmailOrderField { + """ + Order emails by email + """ + EMAIL +} + +""" +Ordering options for Enterprise Server user account connections. +""" +input EnterpriseServerUserAccountOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user accounts by. + """ + field: EnterpriseServerUserAccountOrderField! +} + +""" +Properties by which Enterprise Server user account connections can be ordered. +""" +enum EnterpriseServerUserAccountOrderField { + """ + Order user accounts by login + """ + LOGIN + + """ + Order user accounts by creation time on the Enterprise Server installation + """ + REMOTE_CREATED_AT +} + +""" +A user accounts upload from an Enterprise Server installation. +""" +type EnterpriseServerUserAccountsUpload implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The enterprise to which this upload belongs. + """ + enterprise: Enterprise! + + """ + The Enterprise Server installation for which this upload was generated. + """ + enterpriseServerInstallation: EnterpriseServerInstallation! + id: ID! + + """ + The name of the file uploaded. + """ + name: String! + + """ + The synchronization state of the upload + """ + syncState: EnterpriseServerUserAccountsUploadSyncState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for EnterpriseServerUserAccountsUpload. +""" +type EnterpriseServerUserAccountsUploadConnection { + """ + A list of edges. + """ + edges: [EnterpriseServerUserAccountsUploadEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseServerUserAccountsUpload] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseServerUserAccountsUploadEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseServerUserAccountsUpload +} + +""" +Ordering options for Enterprise Server user accounts upload connections. +""" +input EnterpriseServerUserAccountsUploadOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user accounts uploads by. + """ + field: EnterpriseServerUserAccountsUploadOrderField! +} + +""" +Properties by which Enterprise Server user accounts upload connections can be ordered. +""" +enum EnterpriseServerUserAccountsUploadOrderField { + """ + Order user accounts uploads by creation time + """ + CREATED_AT +} + +""" +Synchronization state of the Enterprise Server user accounts upload +""" +enum EnterpriseServerUserAccountsUploadSyncState { + """ + The synchronization of the upload failed. + """ + FAILURE + + """ + The synchronization of the upload is pending. + """ + PENDING + + """ + The synchronization of the upload succeeded. + """ + SUCCESS +} + +""" +An account for a user who is an admin of an enterprise or a member of an enterprise through one or more organizations. +""" +type EnterpriseUserAccount implements Actor & Node { + """ + A URL pointing to the enterprise user account's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The enterprise in which this user account exists. + """ + enterprise: Enterprise! + id: ID! + + """ + An identifier for the enterprise user account, a login or email address + """ + login: String! + + """ + The name of the enterprise user account + """ + name: String + + """ + A list of enterprise organizations this user is a member of. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for organizations returned from the connection. + """ + orderBy: OrganizationOrder = {field: LOGIN, direction: ASC} + + """ + The search string to look for. + """ + query: String + + """ + The role of the user in the enterprise organization. + """ + role: EnterpriseUserAccountMembershipRole + ): EnterpriseOrganizationMembershipConnection! + + """ + The HTTP path for this user. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this user. + """ + url: URI! + + """ + The user within the enterprise. + """ + user: User +} + +""" +The connection type for EnterpriseUserAccount. +""" +type EnterpriseUserAccountConnection { + """ + A list of edges. + """ + edges: [EnterpriseUserAccountEdge] + + """ + A list of nodes. + """ + nodes: [EnterpriseUserAccount] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type EnterpriseUserAccountEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EnterpriseUserAccount +} + +""" +The possible roles for enterprise membership. +""" +enum EnterpriseUserAccountMembershipRole { + """ + The user is a member of the enterprise membership. + """ + MEMBER + + """ + The user is an owner of the enterprise membership. + """ + OWNER +} + +""" +The possible GitHub Enterprise deployments where this user can exist. +""" +enum EnterpriseUserDeployment { + """ + The user is part of a GitHub Enterprise Cloud deployment. + """ + CLOUD + + """ + The user is part of a GitHub Enterprise Server deployment. + """ + SERVER +} + +""" +An external identity provisioned by SAML SSO or SCIM. +""" +type ExternalIdentity implements Node { + """ + The GUID for this identity + """ + guid: String! + id: ID! + + """ + Organization invitation for this SCIM-provisioned external identity + """ + organizationInvitation: OrganizationInvitation + + """ + SAML Identity attributes + """ + samlIdentity: ExternalIdentitySamlAttributes + + """ + SCIM Identity attributes + """ + scimIdentity: ExternalIdentityScimAttributes + + """ + User linked to this external identity. Will be NULL if this identity has not been claimed by an organization member. + """ + user: User +} + +""" +The connection type for ExternalIdentity. +""" +type ExternalIdentityConnection { + """ + A list of edges. + """ + edges: [ExternalIdentityEdge] + + """ + A list of nodes. + """ + nodes: [ExternalIdentity] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ExternalIdentityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ExternalIdentity +} + +""" +SAML attributes for the External Identity +""" +type ExternalIdentitySamlAttributes { + """ + The emails associated with the SAML identity + """ + emails: [UserEmailMetadata!] + + """ + Family name of the SAML identity + """ + familyName: String + + """ + Given name of the SAML identity + """ + givenName: String + + """ + The groups linked to this identity in IDP + """ + groups: [String!] + + """ + The NameID of the SAML identity + """ + nameId: String + + """ + The userName of the SAML identity + """ + username: String +} + +""" +SCIM attributes for the External Identity +""" +type ExternalIdentityScimAttributes { + """ + The emails associated with the SCIM identity + """ + emails: [UserEmailMetadata!] + + """ + Family name of the SCIM identity + """ + familyName: String + + """ + Given name of the SCIM identity + """ + givenName: String + + """ + The groups linked to this identity in IDP + """ + groups: [String!] + + """ + The userName of the SCIM identity + """ + username: String +} + +""" +The possible viewed states of a file . +""" +enum FileViewedState { + """ + The file has new changes since last viewed. + """ + DISMISSED + + """ + The file has not been marked as viewed. + """ + UNVIEWED + + """ + The file has been marked as viewed. + """ + VIEWED +} + +""" +Autogenerated input type of FollowUser +""" +input FollowUserInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the user to follow. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of FollowUser +""" +type FollowUserPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was followed. + """ + user: User +} + +""" +The connection type for User. +""" +type FollowerConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The connection type for User. +""" +type FollowingConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A funding platform link for a repository. +""" +type FundingLink { + """ + The funding platform this link is for. + """ + platform: FundingPlatform! + + """ + The configured URL for this funding link. + """ + url: URI! +} + +""" +The possible funding platforms for repository funding links. +""" +enum FundingPlatform { + """ + Community Bridge funding platform. + """ + COMMUNITY_BRIDGE + + """ + Custom funding platform. + """ + CUSTOM + + """ + GitHub funding platform. + """ + GITHUB + + """ + IssueHunt funding platform. + """ + ISSUEHUNT + + """ + Ko-fi funding platform. + """ + KO_FI + + """ + Liberapay funding platform. + """ + LIBERAPAY + + """ + Open Collective funding platform. + """ + OPEN_COLLECTIVE + + """ + Otechie funding platform. + """ + OTECHIE + + """ + Patreon funding platform. + """ + PATREON + + """ + Tidelift funding platform. + """ + TIDELIFT +} + +""" +A generic hovercard context with a message and icon +""" +type GenericHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! +} + +""" +A Gist. +""" +type Gist implements Node & Starrable & UniformResourceLocatable { + """ + A list of comments associated with the gist + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GistCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The gist description. + """ + description: String + + """ + The files in this gist. + """ + files( + """ + The maximum number of files to return. + """ + limit: Int = 10 + + """ + The oid of the files to return + """ + oid: GitObjectID + ): [GistFile] + + """ + A list of forks associated with the gist + """ + forks( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for gists returned from the connection + """ + orderBy: GistOrder + ): GistConnection! + id: ID! + + """ + Identifies if the gist is a fork. + """ + isFork: Boolean! + + """ + Whether the gist is public or not. + """ + isPublic: Boolean! + + """ + The gist name. + """ + name: String! + + """ + The gist owner. + """ + owner: RepositoryOwner + + """ + Identifies when the gist was last pushed to. + """ + pushedAt: DateTime + + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this Gist. + """ + url: URI! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +Represents a comment on an Gist. +""" +type GistComment implements Comment & Deletable & Minimizable & Node & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the gist. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the comment body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The associated gist. + """ + gist: Gist! + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for GistComment. +""" +type GistCommentConnection { + """ + A list of edges. + """ + edges: [GistCommentEdge] + + """ + A list of nodes. + """ + nodes: [GistComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GistCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: GistComment +} + +""" +The connection type for Gist. +""" +type GistConnection { + """ + A list of edges. + """ + edges: [GistEdge] + + """ + A list of nodes. + """ + nodes: [Gist] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GistEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Gist +} + +""" +A file in a gist. +""" +type GistFile { + """ + The file name encoded to remove characters that are invalid in URL paths. + """ + encodedName: String + + """ + The gist file encoding. + """ + encoding: String + + """ + The file extension from the file name. + """ + extension: String + + """ + Indicates if this file is an image. + """ + isImage: Boolean! + + """ + Whether the file's contents were truncated. + """ + isTruncated: Boolean! + + """ + The programming language this file is written in. + """ + language: Language + + """ + The gist file name. + """ + name: String + + """ + The gist file size in bytes. + """ + size: Int + + """ + UTF8 text data or null if the file is binary + """ + text( + """ + Optionally truncate the returned file to this length. + """ + truncate: Int + ): String +} + +""" +Ordering options for gist connections +""" +input GistOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: GistOrderField! +} + +""" +Properties by which gist connections can be ordered. +""" +enum GistOrderField { + """ + Order gists by creation time + """ + CREATED_AT + + """ + Order gists by push time + """ + PUSHED_AT + + """ + Order gists by update time + """ + UPDATED_AT +} + +""" +The privacy of a Gist +""" +enum GistPrivacy { + """ + Gists that are public and secret + """ + ALL + + """ + Public + """ + PUBLIC + + """ + Secret + """ + SECRET +} + +""" +Represents an actor in a Git commit (ie. an author or committer). +""" +type GitActor { + """ + A URL pointing to the author's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The timestamp of the Git action (authoring or committing). + """ + date: GitTimestamp + + """ + The email in the Git commit. + """ + email: String + + """ + The name in the Git commit. + """ + name: String + + """ + The GitHub user corresponding to the email field. Null if no such user exists. + """ + user: User +} + +""" +The connection type for GitActor. +""" +type GitActorConnection { + """ + A list of edges. + """ + edges: [GitActorEdge] + + """ + A list of nodes. + """ + nodes: [GitActor] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type GitActorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: GitActor +} + +""" +Represents information about the GitHub instance. +""" +type GitHubMetadata { + """ + Returns a String that's a SHA of `github-services` + """ + gitHubServicesSha: GitObjectID! + + """ + IP addresses that users connect to for git operations + """ + gitIpAddresses: [String!] + + """ + IP addresses that service hooks are sent from + """ + hookIpAddresses: [String!] + + """ + IP addresses that the importer connects from + """ + importerIpAddresses: [String!] + + """ + Whether or not users are verified + """ + isPasswordAuthenticationVerifiable: Boolean! + + """ + IP addresses for GitHub Pages' A records + """ + pagesIpAddresses: [String!] +} + +""" +Represents a Git object. +""" +interface GitObject { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + id: ID! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! +} + +""" +A Git object ID. +""" +scalar GitObjectID + +""" +A fully qualified reference name (e.g. `refs/heads/master`). +""" +scalar GitRefname @preview(toggledBy: "update-refs-preview") + +""" +Git SSH string +""" +scalar GitSSHRemote + +""" +Information about a signature (GPG or S/MIME) on a Commit or Tag. +""" +interface GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +The state of a Git signature. +""" +enum GitSignatureState { + """ + The signing certificate or its chain could not be verified + """ + BAD_CERT + + """ + Invalid email used for signing + """ + BAD_EMAIL + + """ + Signing key expired + """ + EXPIRED_KEY + + """ + Internal error - the GPG verification service misbehaved + """ + GPGVERIFY_ERROR + + """ + Internal error - the GPG verification service is unavailable at the moment + """ + GPGVERIFY_UNAVAILABLE + + """ + Invalid signature + """ + INVALID + + """ + Malformed signature + """ + MALFORMED_SIG + + """ + The usage flags for the key that signed this don't allow signing + """ + NOT_SIGNING_KEY + + """ + Email used for signing not known to GitHub + """ + NO_USER + + """ + Valid signature, though certificate revocation check failed + """ + OCSP_ERROR + + """ + Valid signature, pending certificate revocation checking + """ + OCSP_PENDING + + """ + One or more certificates in chain has been revoked + """ + OCSP_REVOKED + + """ + Key used for signing not known to GitHub + """ + UNKNOWN_KEY + + """ + Unknown signature type + """ + UNKNOWN_SIG_TYPE + + """ + Unsigned + """ + UNSIGNED + + """ + Email used for signing unverified on GitHub + """ + UNVERIFIED_EMAIL + + """ + Valid signature and verified by GitHub + """ + VALID +} + +""" +An ISO-8601 encoded date string. Unlike the DateTime type, GitTimestamp is not converted in UTC. +""" +scalar GitTimestamp + +""" +Represents a GPG signature on a Commit or Tag. +""" +type GpgSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Hex-encoded ID of the key that signed this object. + """ + keyId: String + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +A string containing HTML code. +""" +scalar HTML + +""" +Represents a 'head_ref_deleted' event on a given pull request. +""" +type HeadRefDeletedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the Ref associated with the `head_ref_deleted` event. + """ + headRef: Ref + + """ + Identifies the name of the Ref associated with the `head_ref_deleted` event. + """ + headRefName: String! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Represents a 'head_ref_force_pushed' event on a given pull request. +""" +type HeadRefForcePushedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the after commit SHA for the 'head_ref_force_pushed' event. + """ + afterCommit: Commit + + """ + Identifies the before commit SHA for the 'head_ref_force_pushed' event. + """ + beforeCommit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the fully qualified ref name for the 'head_ref_force_pushed' event. + """ + ref: Ref +} + +""" +Represents a 'head_ref_restored' event on a given pull request. +""" +type HeadRefRestoredEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! +} + +""" +Detail needed to display a hovercard for a user +""" +type Hovercard { + """ + Each of the contexts for this hovercard + """ + contexts: [HovercardContext!]! +} + +""" +An individual line of a hovercard +""" +interface HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! +} + +""" +The possible states in which authentication can be configured with an identity provider. +""" +enum IdentityProviderConfigurationState { + """ + Authentication with an identity provider is configured but not enforced. + """ + CONFIGURED + + """ + Authentication with an identity provider is configured and enforced. + """ + ENFORCED + + """ + Authentication with an identity provider is not configured. + """ + UNCONFIGURED +} + +""" +Autogenerated input type of ImportProject +""" +input ImportProjectInput { + """ + The description of Project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of columns containing issues and pull requests. + """ + columnImports: [ProjectColumnImport!]! + + """ + The name of Project. + """ + name: String! + + """ + The name of the Organization or User to create the Project under. + """ + ownerName: String! + + """ + Whether the Project is public or not. + """ + public: Boolean = false +} + +""" +Autogenerated return type of ImportProject +""" +type ImportProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new Project! + """ + project: Project +} + +""" +Autogenerated input type of InviteEnterpriseAdmin +""" +input InviteEnterpriseAdminInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The email of the person to invite as an administrator. + """ + email: String + + """ + The ID of the enterprise to which you want to invite an administrator. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a user to invite as an administrator. + """ + invitee: String + + """ + The role of the administrator. + """ + role: EnterpriseAdministratorRole +} + +""" +Autogenerated return type of InviteEnterpriseAdmin +""" +type InviteEnterpriseAdminPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The created enterprise administrator invitation. + """ + invitation: EnterpriseAdministratorInvitation +} + +""" +The possible values for the IP allow list enabled setting. +""" +enum IpAllowListEnabledSettingValue { + """ + The setting is disabled for the owner. + """ + DISABLED + + """ + The setting is enabled for the owner. + """ + ENABLED +} + +""" +An IP address or range of addresses that is allowed to access an owner's resources. +""" +type IpAllowListEntry implements Node { + """ + A single IP address or range of IP addresses in CIDR notation. + """ + allowListValue: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Whether the entry is currently active. + """ + isActive: Boolean! + + """ + The name of the IP allow list entry. + """ + name: String + + """ + The owner of the IP allow list entry. + """ + owner: IpAllowListOwner! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +The connection type for IpAllowListEntry. +""" +type IpAllowListEntryConnection { + """ + A list of edges. + """ + edges: [IpAllowListEntryEdge] + + """ + A list of nodes. + """ + nodes: [IpAllowListEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IpAllowListEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IpAllowListEntry +} + +""" +Ordering options for IP allow list entry connections. +""" +input IpAllowListEntryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order IP allow list entries by. + """ + field: IpAllowListEntryOrderField! +} + +""" +Properties by which IP allow list entry connections can be ordered. +""" +enum IpAllowListEntryOrderField { + """ + Order IP allow list entries by the allow list value. + """ + ALLOW_LIST_VALUE + + """ + Order IP allow list entries by creation time. + """ + CREATED_AT +} + +""" +Types that can own an IP allow list. +""" +union IpAllowListOwner = Enterprise | Organization + +""" +An Issue is a place to discuss ideas, enhancements, tasks, and bugs for a project. +""" +type Issue implements Assignable & Closable & Comment & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the body of the issue. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The http path for this issue body + """ + bodyResourcePath: URI! + + """ + Identifies the body of the issue rendered to text. + """ + bodyText: String! + + """ + The http URL for this issue body + """ + bodyUrl: URI! + + """ + `true` if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + A list of comments associated with the Issue. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + + """ + The hovercard information for this issue + """ + hovercard( + """ + Whether or not to include notification contexts + """ + includeNotificationContexts: Boolean = true + ): Hovercard! + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Is this issue read by the viewer + """ + isReadByViewer: Boolean + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + `true` if the object is locked + """ + locked: Boolean! + + """ + Identifies the milestone associated with the issue. + """ + milestone: Milestone + + """ + Identifies the issue number. + """ + number: Int! + + """ + A list of Users that are participating in the Issue conversation. + """ + participants( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + List of project cards associated with this issue. + """ + projectCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this issue + """ + resourcePath: URI! + + """ + Identifies the state of the issue. + """ + state: IssueState! + + """ + A list of events, comments, commits, etc. associated with the issue. + """ + timeline( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows filtering timeline events by a `since` timestamp. + """ + since: DateTime + ): IssueTimelineConnection! @deprecated(reason: "`timeline` will be removed Use Issue.timelineItems instead. Removal on 2020-10-01 UTC.") + + """ + A list of events, comments, commits, etc. associated with the issue. + """ + timelineItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter timeline items by type. + """ + itemTypes: [IssueTimelineItemsItemType!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter timeline items by a `since` timestamp. + """ + since: DateTime + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): IssueTimelineItemsConnection! + + """ + Identifies the issue title. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Represents a comment on an Issue. +""" +type IssueComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + Identifies the issue associated with the comment. + """ + issue: Issue! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. + """ + minimizedReason: String + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Returns the pull request associated with the comment, if this comment was made on a + pull request. + """ + pullRequest: PullRequest + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this issue comment + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue comment + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for IssueComment. +""" +type IssueCommentConnection { + """ + A list of edges. + """ + edges: [IssueCommentEdge] + + """ + A list of nodes. + """ + nodes: [IssueComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type IssueCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueComment +} + +""" +Ways in which lists of issue comments can be ordered upon return. +""" +input IssueCommentOrder { + """ + The direction in which to order issue comments by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order issue comments by. + """ + field: IssueCommentOrderField! +} + +""" +Properties by which issue comment connections can be ordered. +""" +enum IssueCommentOrderField { + """ + Order issue comments by update time + """ + UPDATED_AT +} + +""" +The connection type for Issue. +""" +type IssueConnection { + """ + A list of edges. + """ + edges: [IssueEdge] + + """ + A list of nodes. + """ + nodes: [Issue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates issues opened by a user within one repository. +""" +type IssueContributionsByRepository { + """ + The issue contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedIssueContributionConnection! + + """ + The repository in which the issues were opened. + """ + repository: Repository! +} + +""" +An edge in a connection. +""" +type IssueEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Issue +} + +""" +Ways in which to filter lists of issues. +""" +input IssueFilters { + """ + List issues assigned to given name. Pass in `null` for issues with no assigned + user, and `*` for issues assigned to any user. + """ + assignee: String + + """ + List issues created by given name. + """ + createdBy: String + + """ + List issues where the list of label names exist on the issue. + """ + labels: [String!] + + """ + List issues where the given name is mentioned in the issue. + """ + mentioned: String + + """ + List issues by given milestone argument. If an string representation of an + integer is passed, it should refer to a milestone by its number field. Pass in + `null` for issues with no milestone, and `*` for issues that are assigned to any milestone. + """ + milestone: String + + """ + List issues that have been updated at or after the given date. + """ + since: DateTime + + """ + List issues filtered by the list of states given. + """ + states: [IssueState!] + + """ + List issues subscribed to by viewer. + """ + viewerSubscribed: Boolean = false +} + +""" +Used for return value of Repository.issueOrPullRequest. +""" +union IssueOrPullRequest = Issue | PullRequest + +""" +Ways in which lists of issues can be ordered upon return. +""" +input IssueOrder { + """ + The direction in which to order issues by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order issues by. + """ + field: IssueOrderField! +} + +""" +Properties by which issue connections can be ordered. +""" +enum IssueOrderField { + """ + Order issues by comment count + """ + COMMENTS + + """ + Order issues by creation time + """ + CREATED_AT + + """ + Order issues by update time + """ + UPDATED_AT +} + +""" +The possible states of an issue. +""" +enum IssueState { + """ + An issue that has been closed + """ + CLOSED + + """ + An issue that is still open + """ + OPEN +} + +""" +A repository issue template. +""" +type IssueTemplate { + """ + The template purpose. + """ + about: String + + """ + The suggested issue body. + """ + body: String + + """ + The template name. + """ + name: String! + + """ + The suggested issue title. + """ + title: String +} + +""" +The connection type for IssueTimelineItem. +""" +type IssueTimelineConnection { + """ + A list of edges. + """ + edges: [IssueTimelineItemEdge] + + """ + A list of nodes. + """ + nodes: [IssueTimelineItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An item in an issue timeline +""" +union IssueTimelineItem = AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent | DemilestonedEvent | IssueComment | LabeledEvent | LockedEvent | MilestonedEvent | ReferencedEvent | RenamedTitleEvent | ReopenedEvent | SubscribedEvent | TransferredEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent | UserBlockedEvent + +""" +An edge in a connection. +""" +type IssueTimelineItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueTimelineItem +} + +""" +An item in an issue timeline +""" +union IssueTimelineItems = AddedToProjectEvent | AssignedEvent | ClosedEvent | CommentDeletedEvent | ConnectedEvent | ConvertedNoteToIssueEvent | CrossReferencedEvent | DemilestonedEvent | DisconnectedEvent | IssueComment | LabeledEvent | LockedEvent | MarkedAsDuplicateEvent | MentionedEvent | MilestonedEvent | MovedColumnsInProjectEvent | PinnedEvent | ReferencedEvent | RemovedFromProjectEvent | RenamedTitleEvent | ReopenedEvent | SubscribedEvent | TransferredEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnmarkedAsDuplicateEvent | UnpinnedEvent | UnsubscribedEvent | UserBlockedEvent + +""" +The connection type for IssueTimelineItems. +""" +type IssueTimelineItemsConnection { + """ + A list of edges. + """ + edges: [IssueTimelineItemsEdge] + + """ + Identifies the count of items after applying `before` and `after` filters. + """ + filteredCount: Int! + + """ + A list of nodes. + """ + nodes: [IssueTimelineItems] + + """ + Identifies the count of items after applying `before`/`after` filters and `first`/`last`/`skip` slicing. + """ + pageCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the date and time when the timeline was last updated. + """ + updatedAt: DateTime! +} + +""" +An edge in a connection. +""" +type IssueTimelineItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: IssueTimelineItems +} + +""" +The possible item types found in a timeline. +""" +enum IssueTimelineItemsItemType { + """ + Represents a 'added_to_project' event on a given issue or pull request. + """ + ADDED_TO_PROJECT_EVENT + + """ + Represents an 'assigned' event on any assignable object. + """ + ASSIGNED_EVENT + + """ + Represents a 'closed' event on any `Closable`. + """ + CLOSED_EVENT + + """ + Represents a 'comment_deleted' event on a given issue or pull request. + """ + COMMENT_DELETED_EVENT + + """ + Represents a 'connected' event on a given issue or pull request. + """ + CONNECTED_EVENT + + """ + Represents a 'converted_note_to_issue' event on a given issue or pull request. + """ + CONVERTED_NOTE_TO_ISSUE_EVENT + + """ + Represents a mention made by one issue or pull request to another. + """ + CROSS_REFERENCED_EVENT + + """ + Represents a 'demilestoned' event on a given issue or pull request. + """ + DEMILESTONED_EVENT + + """ + Represents a 'disconnected' event on a given issue or pull request. + """ + DISCONNECTED_EVENT + + """ + Represents a comment on an Issue. + """ + ISSUE_COMMENT + + """ + Represents a 'labeled' event on a given issue or pull request. + """ + LABELED_EVENT + + """ + Represents a 'locked' event on a given issue or pull request. + """ + LOCKED_EVENT + + """ + Represents a 'marked_as_duplicate' event on a given issue or pull request. + """ + MARKED_AS_DUPLICATE_EVENT + + """ + Represents a 'mentioned' event on a given issue or pull request. + """ + MENTIONED_EVENT + + """ + Represents a 'milestoned' event on a given issue or pull request. + """ + MILESTONED_EVENT + + """ + Represents a 'moved_columns_in_project' event on a given issue or pull request. + """ + MOVED_COLUMNS_IN_PROJECT_EVENT + + """ + Represents a 'pinned' event on a given issue or pull request. + """ + PINNED_EVENT + + """ + Represents a 'referenced' event on a given `ReferencedSubject`. + """ + REFERENCED_EVENT + + """ + Represents a 'removed_from_project' event on a given issue or pull request. + """ + REMOVED_FROM_PROJECT_EVENT + + """ + Represents a 'renamed' event on a given issue or pull request + """ + RENAMED_TITLE_EVENT + + """ + Represents a 'reopened' event on any `Closable`. + """ + REOPENED_EVENT + + """ + Represents a 'subscribed' event on a given `Subscribable`. + """ + SUBSCRIBED_EVENT + + """ + Represents a 'transferred' event on a given issue or pull request. + """ + TRANSFERRED_EVENT + + """ + Represents an 'unassigned' event on any assignable object. + """ + UNASSIGNED_EVENT + + """ + Represents an 'unlabeled' event on a given issue or pull request. + """ + UNLABELED_EVENT + + """ + Represents an 'unlocked' event on a given issue or pull request. + """ + UNLOCKED_EVENT + + """ + Represents an 'unmarked_as_duplicate' event on a given issue or pull request. + """ + UNMARKED_AS_DUPLICATE_EVENT + + """ + Represents an 'unpinned' event on a given issue or pull request. + """ + UNPINNED_EVENT + + """ + Represents an 'unsubscribed' event on a given `Subscribable`. + """ + UNSUBSCRIBED_EVENT + + """ + Represents a 'user_blocked' event on a given user. + """ + USER_BLOCKED_EVENT +} + +""" +Represents a user signing up for a GitHub account. +""" +type JoinedGitHubContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A label for categorizing Issues or Milestones with a given Repository. +""" +type Label implements Node { + """ + Identifies the label color. + """ + color: String! + + """ + Identifies the date and time when the label was created. + """ + createdAt: DateTime + + """ + A brief description of this label. + """ + description: String + id: ID! + + """ + Indicates whether or not this is a default label. + """ + isDefault: Boolean! + + """ + A list of issues associated with this label. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Identifies the label name. + """ + name: String! + + """ + A list of pull requests associated with this label. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + The repository associated with this label. + """ + repository: Repository! + + """ + The HTTP path for this label. + """ + resourcePath: URI! + + """ + Identifies the date and time when the label was last updated. + """ + updatedAt: DateTime + + """ + The HTTP URL for this label. + """ + url: URI! +} + +""" +The connection type for Label. +""" +type LabelConnection { + """ + A list of edges. + """ + edges: [LabelEdge] + + """ + A list of nodes. + """ + nodes: [Label] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type LabelEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Label +} + +""" +Ways in which lists of labels can be ordered upon return. +""" +input LabelOrder { + """ + The direction in which to order labels by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order labels by. + """ + field: LabelOrderField! +} + +""" +Properties by which label connections can be ordered. +""" +enum LabelOrderField { + """ + Order labels by creation time + """ + CREATED_AT + + """ + Order labels by name + """ + NAME +} + +""" +An object that can have labels assigned to it. +""" +interface Labelable { + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection +} + +""" +Represents a 'labeled' event on a given issue or pull request. +""" +type LabeledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the label associated with the 'labeled' event. + """ + label: Label! + + """ + Identifies the `Labelable` associated with the event. + """ + labelable: Labelable! +} + +""" +Represents a given language found in repositories. +""" +type Language implements Node { + """ + The color defined for the current language. + """ + color: String + id: ID! + + """ + The name of the current language. + """ + name: String! +} + +""" +A list of languages associated with the parent. +""" +type LanguageConnection { + """ + A list of edges. + """ + edges: [LanguageEdge] + + """ + A list of nodes. + """ + nodes: [Language] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total size in bytes of files written in that language. + """ + totalSize: Int! +} + +""" +Represents the language of a repository. +""" +type LanguageEdge { + cursor: String! + node: Language! + + """ + The number of bytes of code written in the language. + """ + size: Int! +} + +""" +Ordering options for language connections. +""" +input LanguageOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order languages by. + """ + field: LanguageOrderField! +} + +""" +Properties by which language connections can be ordered. +""" +enum LanguageOrderField { + """ + Order languages by the size of all files containing the language + """ + SIZE +} + +""" +A repository's open source license +""" +type License implements Node { + """ + The full text of the license + """ + body: String! + + """ + The conditions set by the license + """ + conditions: [LicenseRule]! + + """ + A human-readable description of the license + """ + description: String + + """ + Whether the license should be featured + """ + featured: Boolean! + + """ + Whether the license should be displayed in license pickers + """ + hidden: Boolean! + id: ID! + + """ + Instructions on how to implement the license + """ + implementation: String + + """ + The lowercased SPDX ID of the license + """ + key: String! + + """ + The limitations set by the license + """ + limitations: [LicenseRule]! + + """ + The license full name specified by + """ + name: String! + + """ + Customary short name if applicable (e.g, GPLv3) + """ + nickname: String + + """ + The permissions set by the license + """ + permissions: [LicenseRule]! + + """ + Whether the license is a pseudo-license placeholder (e.g., other, no-license) + """ + pseudoLicense: Boolean! + + """ + Short identifier specified by + """ + spdxId: String + + """ + URL to the license on + """ + url: URI +} + +""" +Describes a License's conditions, permissions, and limitations +""" +type LicenseRule { + """ + A description of the rule + """ + description: String! + + """ + The machine-readable rule key + """ + key: String! + + """ + The human-readable rule label + """ + label: String! +} + +""" +Autogenerated input type of LinkRepositoryToProject +""" +input LinkRepositoryToProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project to link to a Repository + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The ID of the Repository to link to a Project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of LinkRepositoryToProject +""" +type LinkRepositoryToProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The linked Project. + """ + project: Project + + """ + The linked Repository. + """ + repository: Repository +} + +""" +Autogenerated input type of LockLockable +""" +input LockLockableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A reason for why the item will be locked. + """ + lockReason: LockReason + + """ + ID of the item to be locked. + """ + lockableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Lockable") +} + +""" +Autogenerated return type of LockLockable +""" +type LockLockablePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was locked. + """ + lockedRecord: Lockable +} + +""" +The possible reasons that an issue or pull request was locked. +""" +enum LockReason { + """ + The issue or pull request was locked because the conversation was off-topic. + """ + OFF_TOPIC + + """ + The issue or pull request was locked because the conversation was resolved. + """ + RESOLVED + + """ + The issue or pull request was locked because the conversation was spam. + """ + SPAM + + """ + The issue or pull request was locked because the conversation was too heated. + """ + TOO_HEATED +} + +""" +An object that can be locked. +""" +interface Lockable { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + `true` if the object is locked + """ + locked: Boolean! +} + +""" +Represents a 'locked' event on a given issue or pull request. +""" +type LockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reason that the conversation was locked (optional). + """ + lockReason: LockReason + + """ + Object that was locked. + """ + lockable: Lockable! +} + +""" +A placeholder user for attribution of imported data on GitHub. +""" +type Mannequin implements Actor & Node & UniformResourceLocatable { + """ + A URL pointing to the GitHub App's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The mannequin's email on the source instance. + """ + email: String + id: ID! + + """ + The username of the actor. + """ + login: String! + + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The URL to this resource. + """ + url: URI! +} + +""" +Autogenerated input type of MarkFileAsViewed +""" +input MarkFileAsViewedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The path of the file to mark as viewed + """ + path: String! + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MarkFileAsViewed +""" +type MarkFileAsViewedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of MarkPullRequestReadyForReview +""" +input MarkPullRequestReadyForReviewInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be marked as ready for review. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MarkPullRequestReadyForReview +""" +type MarkPullRequestReadyForReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is ready for review. + """ + pullRequest: PullRequest +} + +""" +Represents a 'marked_as_duplicate' event on a given issue or pull request. +""" +type MarkedAsDuplicateEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + The authoritative issue or pull request which has been duplicated by another. + """ + canonical: IssueOrPullRequest + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The issue or pull request which has been marked as a duplicate of another. + """ + duplicate: IssueOrPullRequest + id: ID! + + """ + Canonical and duplicate belong to different repositories. + """ + isCrossRepository: Boolean! +} + +""" +A public description of a Marketplace category. +""" +type MarketplaceCategory implements Node { + """ + The category's description. + """ + description: String + + """ + The technical description of how apps listed in this category work with GitHub. + """ + howItWorks: String + id: ID! + + """ + The category's name. + """ + name: String! + + """ + How many Marketplace listings have this as their primary category. + """ + primaryListingCount: Int! + + """ + The HTTP path for this Marketplace category. + """ + resourcePath: URI! + + """ + How many Marketplace listings have this as their secondary category. + """ + secondaryListingCount: Int! + + """ + The short name of the category used in its URL. + """ + slug: String! + + """ + The HTTP URL for this Marketplace category. + """ + url: URI! +} + +""" +A listing in the GitHub integration marketplace. +""" +type MarketplaceListing implements Node { + """ + The GitHub App this listing represents. + """ + app: App + + """ + URL to the listing owner's company site. + """ + companyUrl: URI + + """ + The HTTP path for configuring access to the listing's integration or OAuth app + """ + configurationResourcePath: URI! + + """ + The HTTP URL for configuring access to the listing's integration or OAuth app + """ + configurationUrl: URI! + + """ + URL to the listing's documentation. + """ + documentationUrl: URI + + """ + The listing's detailed description. + """ + extendedDescription: String + + """ + The listing's detailed description rendered to HTML. + """ + extendedDescriptionHTML: HTML! + + """ + The listing's introductory description. + """ + fullDescription: String! + + """ + The listing's introductory description rendered to HTML. + """ + fullDescriptionHTML: HTML! + + """ + Does this listing have any plans with a free trial? + """ + hasPublishedFreeTrialPlans: Boolean! + + """ + Does this listing have a terms of service link? + """ + hasTermsOfService: Boolean! + + """ + Whether the creator of the app is a verified org + """ + hasVerifiedOwner: Boolean! + + """ + A technical description of how this app works with GitHub. + """ + howItWorks: String + + """ + The listing's technical description rendered to HTML. + """ + howItWorksHTML: HTML! + id: ID! + + """ + URL to install the product to the viewer's account or organization. + """ + installationUrl: URI + + """ + Whether this listing's app has been installed for the current viewer + """ + installedForViewer: Boolean! + + """ + Whether this listing has been removed from the Marketplace. + """ + isArchived: Boolean! + + """ + Whether this listing is still an editable draft that has not been submitted + for review and is not publicly visible in the Marketplace. + """ + isDraft: Boolean! + + """ + Whether the product this listing represents is available as part of a paid plan. + """ + isPaid: Boolean! + + """ + Whether this listing has been approved for display in the Marketplace. + """ + isPublic: Boolean! + + """ + Whether this listing has been rejected by GitHub for display in the Marketplace. + """ + isRejected: Boolean! + + """ + Whether this listing has been approved for unverified display in the Marketplace. + """ + isUnverified: Boolean! + + """ + Whether this draft listing has been submitted for review for approval to be unverified in the Marketplace. + """ + isUnverifiedPending: Boolean! + + """ + Whether this draft listing has been submitted for review from GitHub for approval to be verified in the Marketplace. + """ + isVerificationPendingFromDraft: Boolean! + + """ + Whether this unverified listing has been submitted for review from GitHub for approval to be verified in the Marketplace. + """ + isVerificationPendingFromUnverified: Boolean! + + """ + Whether this listing has been approved for verified display in the Marketplace. + """ + isVerified: Boolean! + + """ + The hex color code, without the leading '#', for the logo background. + """ + logoBackgroundColor: String! + + """ + URL for the listing's logo image. + """ + logoUrl( + """ + The size in pixels of the resulting square image. + """ + size: Int = 400 + ): URI + + """ + The listing's full name. + """ + name: String! + + """ + The listing's very short description without a trailing period or ampersands. + """ + normalizedShortDescription: String! + + """ + URL to the listing's detailed pricing. + """ + pricingUrl: URI + + """ + The category that best describes the listing. + """ + primaryCategory: MarketplaceCategory! + + """ + URL to the listing's privacy policy, may return an empty string for listings that do not require a privacy policy URL. + """ + privacyPolicyUrl: URI! + + """ + The HTTP path for the Marketplace listing. + """ + resourcePath: URI! + + """ + The URLs for the listing's screenshots. + """ + screenshotUrls: [String]! + + """ + An alternate category that describes the listing. + """ + secondaryCategory: MarketplaceCategory + + """ + The listing's very short description. + """ + shortDescription: String! + + """ + The short name of the listing used in its URL. + """ + slug: String! + + """ + URL to the listing's status page. + """ + statusUrl: URI + + """ + An email address for support for this listing's app. + """ + supportEmail: String + + """ + Either a URL or an email address for support for this listing's app, may + return an empty string for listings that do not require a support URL. + """ + supportUrl: URI! + + """ + URL to the listing's terms of service. + """ + termsOfServiceUrl: URI + + """ + The HTTP URL for the Marketplace listing. + """ + url: URI! + + """ + Can the current viewer add plans for this Marketplace listing. + """ + viewerCanAddPlans: Boolean! + + """ + Can the current viewer approve this Marketplace listing. + """ + viewerCanApprove: Boolean! + + """ + Can the current viewer delist this Marketplace listing. + """ + viewerCanDelist: Boolean! + + """ + Can the current viewer edit this Marketplace listing. + """ + viewerCanEdit: Boolean! + + """ + Can the current viewer edit the primary and secondary category of this + Marketplace listing. + """ + viewerCanEditCategories: Boolean! + + """ + Can the current viewer edit the plans for this Marketplace listing. + """ + viewerCanEditPlans: Boolean! + + """ + Can the current viewer return this Marketplace listing to draft state + so it becomes editable again. + """ + viewerCanRedraft: Boolean! + + """ + Can the current viewer reject this Marketplace listing by returning it to + an editable draft state or rejecting it entirely. + """ + viewerCanReject: Boolean! + + """ + Can the current viewer request this listing be reviewed for display in + the Marketplace as verified. + """ + viewerCanRequestApproval: Boolean! + + """ + Indicates whether the current user has an active subscription to this Marketplace listing. + """ + viewerHasPurchased: Boolean! + + """ + Indicates if the current user has purchased a subscription to this Marketplace listing + for all of the organizations the user owns. + """ + viewerHasPurchasedForAllOrganizations: Boolean! + + """ + Does the current viewer role allow them to administer this Marketplace listing. + """ + viewerIsListingAdmin: Boolean! +} + +""" +Look up Marketplace Listings +""" +type MarketplaceListingConnection { + """ + A list of edges. + """ + edges: [MarketplaceListingEdge] + + """ + A list of nodes. + """ + nodes: [MarketplaceListing] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MarketplaceListingEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MarketplaceListing +} + +""" +Entities that have members who can set status messages. +""" +interface MemberStatusable { + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! +} + +""" +Audit log entry for a members_can_delete_repos.clear event. +""" +type MembersCanDeleteReposClearAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a members_can_delete_repos.disable event. +""" +type MembersCanDeleteReposDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a members_can_delete_repos.enable event. +""" +type MembersCanDeleteReposEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Represents a 'mentioned' event on a given issue or pull request. +""" +type MentionedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! +} + +""" +Autogenerated input type of MergeBranch +""" +input MergeBranchInput { + """ + The email address to associate with this commit. + """ + authorEmail: String + + """ + The name of the base branch that the provided head will be merged into. + """ + base: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Message to use for the merge commit. If omitted, a default will be used. + """ + commitMessage: String + + """ + The head to merge into the base branch. This can be a branch name or a commit GitObjectID. + """ + head: String! + + """ + The Node ID of the Repository containing the base branch that will be modified. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of MergeBranch +""" +type MergeBranchPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The resulting merge Commit. + """ + mergeCommit: Commit +} + +""" +Autogenerated input type of MergePullRequest +""" +input MergePullRequestInput { + """ + The email address to associate with this merge. + """ + authorEmail: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Commit body to use for the merge commit; if omitted, a default message will be used + """ + commitBody: String + + """ + Commit headline to use for the merge commit; if omitted, a default message will be used. + """ + commitHeadline: String + + """ + OID that the pull request head ref must match to allow merge; if omitted, no check is performed. + """ + expectedHeadOid: GitObjectID + + """ + The merge method to use. If omitted, defaults to 'MERGE' + """ + mergeMethod: PullRequestMergeMethod = MERGE + + """ + ID of the pull request to be merged. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of MergePullRequest +""" +type MergePullRequestPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was merged. + """ + pullRequest: PullRequest +} + +""" +Detailed status information about a pull request merge. +""" +enum MergeStateStatus { + """ + The head ref is out of date. + """ + BEHIND + + """ + The merge is blocked. + """ + BLOCKED + + """ + Mergeable and passing commit status. + """ + CLEAN + + """ + The merge commit cannot be cleanly created. + """ + DIRTY + + """ + The merge is blocked due to the pull request being a draft. + """ + DRAFT @deprecated(reason: "DRAFT state will be removed from this enum and `isDraft` should be used instead Use PullRequest.isDraft instead. Removal on 2021-01-01 UTC.") + + """ + Mergeable with passing commit status and pre-receive hooks. + """ + HAS_HOOKS + + """ + The state cannot currently be determined. + """ + UNKNOWN + + """ + Mergeable with non-passing commit status. + """ + UNSTABLE +} + +""" +Whether or not a PullRequest can be merged. +""" +enum MergeableState { + """ + The pull request cannot be merged due to merge conflicts. + """ + CONFLICTING + + """ + The pull request can be merged. + """ + MERGEABLE + + """ + The mergeability of the pull request is still being calculated. + """ + UNKNOWN +} + +""" +Represents a 'merged' event on a given pull request. +""" +type MergedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the commit associated with the `merge` event. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the Ref associated with the `merge` event. + """ + mergeRef: Ref + + """ + Identifies the name of the Ref associated with the `merge` event. + """ + mergeRefName: String! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this merged event. + """ + resourcePath: URI! + + """ + The HTTP URL for this merged event. + """ + url: URI! +} + +""" +Represents a Milestone object on a given repository. +""" +type Milestone implements Closable & Node & UniformResourceLocatable { + """ + `true` if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the actor who created the milestone. + """ + creator: Actor + + """ + Identifies the description of the milestone. + """ + description: String + + """ + Identifies the due date of the milestone. + """ + dueOn: DateTime + id: ID! + + """ + A list of issues associated with the milestone. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Identifies the number of the milestone. + """ + number: Int! + + """ + Identifies the percentage complete for the milestone + """ + progressPercentage: Float! + + """ + A list of pull requests associated with the milestone. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + The repository associated with this milestone. + """ + repository: Repository! + + """ + The HTTP path for this milestone + """ + resourcePath: URI! + + """ + Identifies the state of the milestone. + """ + state: MilestoneState! + + """ + Identifies the title of the milestone. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this milestone + """ + url: URI! +} + +""" +The connection type for Milestone. +""" +type MilestoneConnection { + """ + A list of edges. + """ + edges: [MilestoneEdge] + + """ + A list of nodes. + """ + nodes: [Milestone] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MilestoneEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Milestone +} + +""" +Types that can be inside a Milestone. +""" +union MilestoneItem = Issue | PullRequest + +""" +Ordering options for milestone connections. +""" +input MilestoneOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order milestones by. + """ + field: MilestoneOrderField! +} + +""" +Properties by which milestone connections can be ordered. +""" +enum MilestoneOrderField { + """ + Order milestones by when they were created. + """ + CREATED_AT + + """ + Order milestones by when they are due. + """ + DUE_DATE + + """ + Order milestones by their number. + """ + NUMBER + + """ + Order milestones by when they were last updated. + """ + UPDATED_AT +} + +""" +The possible states of a milestone. +""" +enum MilestoneState { + """ + A milestone that has been closed. + """ + CLOSED + + """ + A milestone that is still open. + """ + OPEN +} + +""" +Represents a 'milestoned' event on a given issue or pull request. +""" +type MilestonedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the milestone title associated with the 'milestoned' event. + """ + milestoneTitle: String! + + """ + Object referenced by event. + """ + subject: MilestoneItem! +} + +""" +Entities that can be minimized. +""" +interface Minimizable { + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + Returns why the comment was minimized. + """ + minimizedReason: String + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! +} + +""" +Autogenerated input type of MinimizeComment +""" +input MinimizeCommentInput { + """ + The classification of comment + """ + classifier: ReportedContentClassifiers! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["CommitComment", "GistComment", "IssueComment", "PullRequestReviewComment"], abstractType: "Minimizable") +} + +""" +Autogenerated return type of MinimizeComment +""" +type MinimizeCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The comment that was minimized. + """ + minimizedComment: Minimizable +} + +""" +Autogenerated input type of MoveProjectCard +""" +input MoveProjectCardInput { + """ + Place the new card after the card with this id. Pass null to place it at the top. + """ + afterCardId: ID @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + The id of the card to move. + """ + cardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to move it into. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of MoveProjectCard +""" +type MoveProjectCardPayload { + """ + The new edge of the moved card. + """ + cardEdge: ProjectCardEdge + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of MoveProjectColumn +""" +input MoveProjectColumnInput { + """ + Place the new column after the column with this id. Pass null to place it at the front. + """ + afterColumnId: ID @possibleTypes(concreteTypes: ["ProjectColumn"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the column to move. + """ + columnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of MoveProjectColumn +""" +type MoveProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new edge of the moved column. + """ + columnEdge: ProjectColumnEdge +} + +""" +Represents a 'moved_columns_in_project' event on a given issue or pull request. +""" +type MovedColumnsInProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Column name the issue or pull request was moved from. + """ + previousProjectColumnName: String! @preview(toggledBy: "starfox-preview") + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Project card referenced by this project event. + """ + projectCard: ProjectCard @preview(toggledBy: "starfox-preview") + + """ + Column name the issue or pull request was moved to. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +The root query for implementing GraphQL mutations. +""" +type Mutation { + """ + Accepts a pending invitation for a user to become an administrator of an enterprise. + """ + acceptEnterpriseAdministratorInvitation(input: AcceptEnterpriseAdministratorInvitationInput!): AcceptEnterpriseAdministratorInvitationPayload + + """ + Applies a suggested topic to the repository. + """ + acceptTopicSuggestion(input: AcceptTopicSuggestionInput!): AcceptTopicSuggestionPayload + + """ + Adds assignees to an assignable object. + """ + addAssigneesToAssignable(input: AddAssigneesToAssignableInput!): AddAssigneesToAssignablePayload + + """ + Adds a comment to an Issue or Pull Request. + """ + addComment(input: AddCommentInput!): AddCommentPayload + + """ + Adds a support entitlement to an enterprise member. + """ + addEnterpriseSupportEntitlement(input: AddEnterpriseSupportEntitlementInput!): AddEnterpriseSupportEntitlementPayload + + """ + Adds labels to a labelable object. + """ + addLabelsToLabelable(input: AddLabelsToLabelableInput!): AddLabelsToLabelablePayload + + """ + Adds a card to a ProjectColumn. Either `contentId` or `note` must be provided but **not** both. + """ + addProjectCard(input: AddProjectCardInput!): AddProjectCardPayload + + """ + Adds a column to a Project. + """ + addProjectColumn(input: AddProjectColumnInput!): AddProjectColumnPayload + + """ + Adds a review to a Pull Request. + """ + addPullRequestReview(input: AddPullRequestReviewInput!): AddPullRequestReviewPayload + + """ + Adds a comment to a review. + """ + addPullRequestReviewComment(input: AddPullRequestReviewCommentInput!): AddPullRequestReviewCommentPayload + + """ + Adds a new thread to a pending Pull Request Review. + """ + addPullRequestReviewThread(input: AddPullRequestReviewThreadInput!): AddPullRequestReviewThreadPayload + + """ + Adds a reaction to a subject. + """ + addReaction(input: AddReactionInput!): AddReactionPayload + + """ + Adds a star to a Starrable. + """ + addStar(input: AddStarInput!): AddStarPayload + + """ + Adds a verifiable domain to an owning account. + """ + addVerifiableDomain(input: AddVerifiableDomainInput!): AddVerifiableDomainPayload + + """ + Marks a repository as archived. + """ + archiveRepository(input: ArchiveRepositoryInput!): ArchiveRepositoryPayload + + """ + Cancels a pending invitation for an administrator to join an enterprise. + """ + cancelEnterpriseAdminInvitation(input: CancelEnterpriseAdminInvitationInput!): CancelEnterpriseAdminInvitationPayload + + """ + Update your status on GitHub. + """ + changeUserStatus(input: ChangeUserStatusInput!): ChangeUserStatusPayload + + """ + Clears all labels from a labelable object. + """ + clearLabelsFromLabelable(input: ClearLabelsFromLabelableInput!): ClearLabelsFromLabelablePayload + + """ + Creates a new project by cloning configuration from an existing project. + """ + cloneProject(input: CloneProjectInput!): CloneProjectPayload + + """ + Create a new repository with the same files and directory structure as a template repository. + """ + cloneTemplateRepository(input: CloneTemplateRepositoryInput!): CloneTemplateRepositoryPayload + + """ + Close an issue. + """ + closeIssue(input: CloseIssueInput!): CloseIssuePayload + + """ + Close a pull request. + """ + closePullRequest(input: ClosePullRequestInput!): ClosePullRequestPayload + + """ + Convert a project note card to one associated with a newly created issue. + """ + convertProjectCardNoteToIssue(input: ConvertProjectCardNoteToIssueInput!): ConvertProjectCardNoteToIssuePayload + + """ + Create a new branch protection rule + """ + createBranchProtectionRule(input: CreateBranchProtectionRuleInput!): CreateBranchProtectionRulePayload + + """ + Create a check run. + """ + createCheckRun(input: CreateCheckRunInput!): CreateCheckRunPayload + + """ + Create a check suite + """ + createCheckSuite(input: CreateCheckSuiteInput!): CreateCheckSuitePayload + + """ + Create a content attachment. + """ + createContentAttachment(input: CreateContentAttachmentInput!): CreateContentAttachmentPayload @preview(toggledBy: "corsair-preview") + + """ + Creates a new deployment event. + """ + createDeployment(input: CreateDeploymentInput!): CreateDeploymentPayload @preview(toggledBy: "flash-preview") + + """ + Create a deployment status. + """ + createDeploymentStatus(input: CreateDeploymentStatusInput!): CreateDeploymentStatusPayload @preview(toggledBy: "flash-preview") + + """ + Creates an organization as part of an enterprise account. + """ + createEnterpriseOrganization(input: CreateEnterpriseOrganizationInput!): CreateEnterpriseOrganizationPayload + + """ + Creates a new IP allow list entry. + """ + createIpAllowListEntry(input: CreateIpAllowListEntryInput!): CreateIpAllowListEntryPayload + + """ + Creates a new issue. + """ + createIssue(input: CreateIssueInput!): CreateIssuePayload + + """ + Creates a new label. + """ + createLabel(input: CreateLabelInput!): CreateLabelPayload @preview(toggledBy: "bane-preview") + + """ + Creates a new project. + """ + createProject(input: CreateProjectInput!): CreateProjectPayload + + """ + Create a new pull request + """ + createPullRequest(input: CreatePullRequestInput!): CreatePullRequestPayload + + """ + Create a new Git Ref. + """ + createRef(input: CreateRefInput!): CreateRefPayload + + """ + Create a new repository. + """ + createRepository(input: CreateRepositoryInput!): CreateRepositoryPayload + + """ + Creates a new team discussion. + """ + createTeamDiscussion(input: CreateTeamDiscussionInput!): CreateTeamDiscussionPayload + + """ + Creates a new team discussion comment. + """ + createTeamDiscussionComment(input: CreateTeamDiscussionCommentInput!): CreateTeamDiscussionCommentPayload + + """ + Rejects a suggested topic for the repository. + """ + declineTopicSuggestion(input: DeclineTopicSuggestionInput!): DeclineTopicSuggestionPayload + + """ + Delete a branch protection rule + """ + deleteBranchProtectionRule(input: DeleteBranchProtectionRuleInput!): DeleteBranchProtectionRulePayload + + """ + Deletes a deployment. + """ + deleteDeployment(input: DeleteDeploymentInput!): DeleteDeploymentPayload + + """ + Deletes an IP allow list entry. + """ + deleteIpAllowListEntry(input: DeleteIpAllowListEntryInput!): DeleteIpAllowListEntryPayload + + """ + Deletes an Issue object. + """ + deleteIssue(input: DeleteIssueInput!): DeleteIssuePayload + + """ + Deletes an IssueComment object. + """ + deleteIssueComment(input: DeleteIssueCommentInput!): DeleteIssueCommentPayload + + """ + Deletes a label. + """ + deleteLabel(input: DeleteLabelInput!): DeleteLabelPayload @preview(toggledBy: "bane-preview") + + """ + Delete a package version. + """ + deletePackageVersion(input: DeletePackageVersionInput!): DeletePackageVersionPayload @preview(toggledBy: "package-deletes-preview") + + """ + Deletes a project. + """ + deleteProject(input: DeleteProjectInput!): DeleteProjectPayload + + """ + Deletes a project card. + """ + deleteProjectCard(input: DeleteProjectCardInput!): DeleteProjectCardPayload + + """ + Deletes a project column. + """ + deleteProjectColumn(input: DeleteProjectColumnInput!): DeleteProjectColumnPayload + + """ + Deletes a pull request review. + """ + deletePullRequestReview(input: DeletePullRequestReviewInput!): DeletePullRequestReviewPayload + + """ + Deletes a pull request review comment. + """ + deletePullRequestReviewComment(input: DeletePullRequestReviewCommentInput!): DeletePullRequestReviewCommentPayload + + """ + Delete a Git Ref. + """ + deleteRef(input: DeleteRefInput!): DeleteRefPayload + + """ + Deletes a team discussion. + """ + deleteTeamDiscussion(input: DeleteTeamDiscussionInput!): DeleteTeamDiscussionPayload + + """ + Deletes a team discussion comment. + """ + deleteTeamDiscussionComment(input: DeleteTeamDiscussionCommentInput!): DeleteTeamDiscussionCommentPayload + + """ + Deletes a verifiable domain. + """ + deleteVerifiableDomain(input: DeleteVerifiableDomainInput!): DeleteVerifiableDomainPayload + + """ + Dismisses an approved or rejected pull request review. + """ + dismissPullRequestReview(input: DismissPullRequestReviewInput!): DismissPullRequestReviewPayload + + """ + Follow a user. + """ + followUser(input: FollowUserInput!): FollowUserPayload + + """ + Creates a new project by importing columns and a list of issues/PRs. + """ + importProject(input: ImportProjectInput!): ImportProjectPayload @preview(toggledBy: "slothette-preview") + + """ + Invite someone to become an administrator of the enterprise. + """ + inviteEnterpriseAdmin(input: InviteEnterpriseAdminInput!): InviteEnterpriseAdminPayload + + """ + Creates a repository link for a project. + """ + linkRepositoryToProject(input: LinkRepositoryToProjectInput!): LinkRepositoryToProjectPayload + + """ + Lock a lockable object + """ + lockLockable(input: LockLockableInput!): LockLockablePayload + + """ + Mark a pull request file as viewed + """ + markFileAsViewed(input: MarkFileAsViewedInput!): MarkFileAsViewedPayload + + """ + Marks a pull request ready for review. + """ + markPullRequestReadyForReview(input: MarkPullRequestReadyForReviewInput!): MarkPullRequestReadyForReviewPayload + + """ + Merge a head into a branch. + """ + mergeBranch(input: MergeBranchInput!): MergeBranchPayload + + """ + Merge a pull request. + """ + mergePullRequest(input: MergePullRequestInput!): MergePullRequestPayload + + """ + Minimizes a comment on an Issue, Commit, Pull Request, or Gist + """ + minimizeComment(input: MinimizeCommentInput!): MinimizeCommentPayload + + """ + Moves a project card to another place. + """ + moveProjectCard(input: MoveProjectCardInput!): MoveProjectCardPayload + + """ + Moves a project column to another place. + """ + moveProjectColumn(input: MoveProjectColumnInput!): MoveProjectColumnPayload + + """ + Pin an issue to a repository + """ + pinIssue(input: PinIssueInput!): PinIssuePayload @preview(toggledBy: "elektra-preview") + + """ + Regenerates the identity provider recovery codes for an enterprise + """ + regenerateEnterpriseIdentityProviderRecoveryCodes(input: RegenerateEnterpriseIdentityProviderRecoveryCodesInput!): RegenerateEnterpriseIdentityProviderRecoveryCodesPayload + + """ + Regenerates a verifiable domain's verification token. + """ + regenerateVerifiableDomainToken(input: RegenerateVerifiableDomainTokenInput!): RegenerateVerifiableDomainTokenPayload + + """ + Removes assignees from an assignable object. + """ + removeAssigneesFromAssignable(input: RemoveAssigneesFromAssignableInput!): RemoveAssigneesFromAssignablePayload + + """ + Removes an administrator from the enterprise. + """ + removeEnterpriseAdmin(input: RemoveEnterpriseAdminInput!): RemoveEnterpriseAdminPayload + + """ + Removes the identity provider from an enterprise + """ + removeEnterpriseIdentityProvider(input: RemoveEnterpriseIdentityProviderInput!): RemoveEnterpriseIdentityProviderPayload + + """ + Removes an organization from the enterprise + """ + removeEnterpriseOrganization(input: RemoveEnterpriseOrganizationInput!): RemoveEnterpriseOrganizationPayload + + """ + Removes a support entitlement from an enterprise member. + """ + removeEnterpriseSupportEntitlement(input: RemoveEnterpriseSupportEntitlementInput!): RemoveEnterpriseSupportEntitlementPayload + + """ + Removes labels from a Labelable object. + """ + removeLabelsFromLabelable(input: RemoveLabelsFromLabelableInput!): RemoveLabelsFromLabelablePayload + + """ + Removes outside collaborator from all repositories in an organization. + """ + removeOutsideCollaborator(input: RemoveOutsideCollaboratorInput!): RemoveOutsideCollaboratorPayload + + """ + Removes a reaction from a subject. + """ + removeReaction(input: RemoveReactionInput!): RemoveReactionPayload + + """ + Removes a star from a Starrable. + """ + removeStar(input: RemoveStarInput!): RemoveStarPayload + + """ + Reopen a issue. + """ + reopenIssue(input: ReopenIssueInput!): ReopenIssuePayload + + """ + Reopen a pull request. + """ + reopenPullRequest(input: ReopenPullRequestInput!): ReopenPullRequestPayload + + """ + Set review requests on a pull request. + """ + requestReviews(input: RequestReviewsInput!): RequestReviewsPayload + + """ + Rerequests an existing check suite. + """ + rerequestCheckSuite(input: RerequestCheckSuiteInput!): RerequestCheckSuitePayload + + """ + Marks a review thread as resolved. + """ + resolveReviewThread(input: ResolveReviewThreadInput!): ResolveReviewThreadPayload + + """ + Creates or updates the identity provider for an enterprise. + """ + setEnterpriseIdentityProvider(input: SetEnterpriseIdentityProviderInput!): SetEnterpriseIdentityProviderPayload + + """ + Set an organization level interaction limit for an organization's public repositories. + """ + setOrganizationInteractionLimit(input: SetOrganizationInteractionLimitInput!): SetOrganizationInteractionLimitPayload + + """ + Sets an interaction limit setting for a repository. + """ + setRepositoryInteractionLimit(input: SetRepositoryInteractionLimitInput!): SetRepositoryInteractionLimitPayload + + """ + Set a user level interaction limit for an user's public repositories. + """ + setUserInteractionLimit(input: SetUserInteractionLimitInput!): SetUserInteractionLimitPayload + + """ + Submits a pending pull request review. + """ + submitPullRequestReview(input: SubmitPullRequestReviewInput!): SubmitPullRequestReviewPayload + + """ + Transfer an issue to a different repository + """ + transferIssue(input: TransferIssueInput!): TransferIssuePayload + + """ + Unarchives a repository. + """ + unarchiveRepository(input: UnarchiveRepositoryInput!): UnarchiveRepositoryPayload + + """ + Unfollow a user. + """ + unfollowUser(input: UnfollowUserInput!): UnfollowUserPayload + + """ + Deletes a repository link from a project. + """ + unlinkRepositoryFromProject(input: UnlinkRepositoryFromProjectInput!): UnlinkRepositoryFromProjectPayload + + """ + Unlock a lockable object + """ + unlockLockable(input: UnlockLockableInput!): UnlockLockablePayload + + """ + Unmark a pull request file as viewed + """ + unmarkFileAsViewed(input: UnmarkFileAsViewedInput!): UnmarkFileAsViewedPayload + + """ + Unmark an issue as a duplicate of another issue. + """ + unmarkIssueAsDuplicate(input: UnmarkIssueAsDuplicateInput!): UnmarkIssueAsDuplicatePayload + + """ + Unminimizes a comment on an Issue, Commit, Pull Request, or Gist + """ + unminimizeComment(input: UnminimizeCommentInput!): UnminimizeCommentPayload + + """ + Unpin a pinned issue from a repository + """ + unpinIssue(input: UnpinIssueInput!): UnpinIssuePayload @preview(toggledBy: "elektra-preview") + + """ + Marks a review thread as unresolved. + """ + unresolveReviewThread(input: UnresolveReviewThreadInput!): UnresolveReviewThreadPayload + + """ + Create a new branch protection rule + """ + updateBranchProtectionRule(input: UpdateBranchProtectionRuleInput!): UpdateBranchProtectionRulePayload + + """ + Update a check run + """ + updateCheckRun(input: UpdateCheckRunInput!): UpdateCheckRunPayload + + """ + Modifies the settings of an existing check suite + """ + updateCheckSuitePreferences(input: UpdateCheckSuitePreferencesInput!): UpdateCheckSuitePreferencesPayload + + """ + Updates the role of an enterprise administrator. + """ + updateEnterpriseAdministratorRole(input: UpdateEnterpriseAdministratorRoleInput!): UpdateEnterpriseAdministratorRolePayload + + """ + Sets whether private repository forks are enabled for an enterprise. + """ + updateEnterpriseAllowPrivateRepositoryForkingSetting(input: UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput!): UpdateEnterpriseAllowPrivateRepositoryForkingSettingPayload + + """ + Sets the default repository permission for organizations in an enterprise. + """ + updateEnterpriseDefaultRepositoryPermissionSetting(input: UpdateEnterpriseDefaultRepositoryPermissionSettingInput!): UpdateEnterpriseDefaultRepositoryPermissionSettingPayload + + """ + Sets whether organization members with admin permissions on a repository can change repository visibility. + """ + updateEnterpriseMembersCanChangeRepositoryVisibilitySetting(input: UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingInput!): UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingPayload + + """ + Sets the members can create repositories setting for an enterprise. + """ + updateEnterpriseMembersCanCreateRepositoriesSetting(input: UpdateEnterpriseMembersCanCreateRepositoriesSettingInput!): UpdateEnterpriseMembersCanCreateRepositoriesSettingPayload + + """ + Sets the members can delete issues setting for an enterprise. + """ + updateEnterpriseMembersCanDeleteIssuesSetting(input: UpdateEnterpriseMembersCanDeleteIssuesSettingInput!): UpdateEnterpriseMembersCanDeleteIssuesSettingPayload + + """ + Sets the members can delete repositories setting for an enterprise. + """ + updateEnterpriseMembersCanDeleteRepositoriesSetting(input: UpdateEnterpriseMembersCanDeleteRepositoriesSettingInput!): UpdateEnterpriseMembersCanDeleteRepositoriesSettingPayload + + """ + Sets whether members can invite collaborators are enabled for an enterprise. + """ + updateEnterpriseMembersCanInviteCollaboratorsSetting(input: UpdateEnterpriseMembersCanInviteCollaboratorsSettingInput!): UpdateEnterpriseMembersCanInviteCollaboratorsSettingPayload + + """ + Sets whether or not an organization admin can make purchases. + """ + updateEnterpriseMembersCanMakePurchasesSetting(input: UpdateEnterpriseMembersCanMakePurchasesSettingInput!): UpdateEnterpriseMembersCanMakePurchasesSettingPayload + + """ + Sets the members can update protected branches setting for an enterprise. + """ + updateEnterpriseMembersCanUpdateProtectedBranchesSetting(input: UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingInput!): UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingPayload + + """ + Sets the members can view dependency insights for an enterprise. + """ + updateEnterpriseMembersCanViewDependencyInsightsSetting(input: UpdateEnterpriseMembersCanViewDependencyInsightsSettingInput!): UpdateEnterpriseMembersCanViewDependencyInsightsSettingPayload + + """ + Sets whether organization projects are enabled for an enterprise. + """ + updateEnterpriseOrganizationProjectsSetting(input: UpdateEnterpriseOrganizationProjectsSettingInput!): UpdateEnterpriseOrganizationProjectsSettingPayload + + """ + Updates an enterprise's profile. + """ + updateEnterpriseProfile(input: UpdateEnterpriseProfileInput!): UpdateEnterpriseProfilePayload + + """ + Sets whether repository projects are enabled for a enterprise. + """ + updateEnterpriseRepositoryProjectsSetting(input: UpdateEnterpriseRepositoryProjectsSettingInput!): UpdateEnterpriseRepositoryProjectsSettingPayload + + """ + Sets whether team discussions are enabled for an enterprise. + """ + updateEnterpriseTeamDiscussionsSetting(input: UpdateEnterpriseTeamDiscussionsSettingInput!): UpdateEnterpriseTeamDiscussionsSettingPayload + + """ + Sets whether two factor authentication is required for all users in an enterprise. + """ + updateEnterpriseTwoFactorAuthenticationRequiredSetting(input: UpdateEnterpriseTwoFactorAuthenticationRequiredSettingInput!): UpdateEnterpriseTwoFactorAuthenticationRequiredSettingPayload + + """ + Sets whether an IP allow list is enabled on an owner. + """ + updateIpAllowListEnabledSetting(input: UpdateIpAllowListEnabledSettingInput!): UpdateIpAllowListEnabledSettingPayload + + """ + Updates an IP allow list entry. + """ + updateIpAllowListEntry(input: UpdateIpAllowListEntryInput!): UpdateIpAllowListEntryPayload + + """ + Updates an Issue. + """ + updateIssue(input: UpdateIssueInput!): UpdateIssuePayload + + """ + Updates an IssueComment object. + """ + updateIssueComment(input: UpdateIssueCommentInput!): UpdateIssueCommentPayload + + """ + Updates an existing label. + """ + updateLabel(input: UpdateLabelInput!): UpdateLabelPayload @preview(toggledBy: "bane-preview") + + """ + Updates an existing project. + """ + updateProject(input: UpdateProjectInput!): UpdateProjectPayload + + """ + Updates an existing project card. + """ + updateProjectCard(input: UpdateProjectCardInput!): UpdateProjectCardPayload + + """ + Updates an existing project column. + """ + updateProjectColumn(input: UpdateProjectColumnInput!): UpdateProjectColumnPayload + + """ + Update a pull request + """ + updatePullRequest(input: UpdatePullRequestInput!): UpdatePullRequestPayload + + """ + Updates the body of a pull request review. + """ + updatePullRequestReview(input: UpdatePullRequestReviewInput!): UpdatePullRequestReviewPayload + + """ + Updates a pull request review comment. + """ + updatePullRequestReviewComment(input: UpdatePullRequestReviewCommentInput!): UpdatePullRequestReviewCommentPayload + + """ + Update a Git Ref. + """ + updateRef(input: UpdateRefInput!): UpdateRefPayload + + """ + Creates, updates and/or deletes multiple refs in a repository. + + This mutation takes a list of `RefUpdate`s and performs these updates + on the repository. All updates are performed atomically, meaning that + if one of them is rejected, no other ref will be modified. + + `RefUpdate.beforeOid` specifies that the given reference needs to point + to the given value before performing any updates. A value of + `0000000000000000000000000000000000000000` can be used to verify that + the references should not exist. + + `RefUpdate.afterOid` specifies the value that the given reference + will point to after performing all updates. A value of + `0000000000000000000000000000000000000000` can be used to delete a + reference. + + If `RefUpdate.force` is set to `true`, a non-fast-forward updates + for the given reference will be allowed. + """ + updateRefs(input: UpdateRefsInput!): UpdateRefsPayload @preview(toggledBy: "update-refs-preview") + + """ + Update information about a repository. + """ + updateRepository(input: UpdateRepositoryInput!): UpdateRepositoryPayload + + """ + Updates the state for subscribable subjects. + """ + updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionPayload + + """ + Updates a team discussion. + """ + updateTeamDiscussion(input: UpdateTeamDiscussionInput!): UpdateTeamDiscussionPayload + + """ + Updates a discussion comment. + """ + updateTeamDiscussionComment(input: UpdateTeamDiscussionCommentInput!): UpdateTeamDiscussionCommentPayload + + """ + Updates team review assignment. + """ + updateTeamReviewAssignment(input: UpdateTeamReviewAssignmentInput!): UpdateTeamReviewAssignmentPayload @preview(toggledBy: "stone-crop-preview") + + """ + Replaces the repository's topics with the given topics. + """ + updateTopics(input: UpdateTopicsInput!): UpdateTopicsPayload + + """ + Verify that a verifiable domain has the expected DNS record. + """ + verifyVerifiableDomain(input: VerifyVerifiableDomainInput!): VerifyVerifiableDomainPayload +} + +""" +An object with an ID. +""" +interface Node { + """ + ID of the object. + """ + id: ID! +} + +""" +Metadata for an audit entry with action oauth_application.* +""" +interface OauthApplicationAuditEntryData { + """ + The name of the OAuth Application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth Application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth Application + """ + oauthApplicationUrl: URI +} + +""" +Audit log entry for a oauth_application.create event. +""" +type OauthApplicationCreateAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The application URL of the OAuth Application. + """ + applicationUrl: URI + + """ + The callback URL of the OAuth Application. + """ + callbackUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth Application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth Application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth Application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The rate limit of the OAuth Application. + """ + rateLimit: Int + + """ + The state of the OAuth Application. + """ + state: OauthApplicationCreateAuditEntryState + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The state of an OAuth Application when it was created. +""" +enum OauthApplicationCreateAuditEntryState { + """ + The OAuth Application was active and allowed to have OAuth Accesses. + """ + ACTIVE + + """ + The OAuth Application was in the process of being deleted. + """ + PENDING_DELETION + + """ + The OAuth Application was suspended from generating OAuth Accesses due to abuse or security concerns. + """ + SUSPENDED +} + +""" +The corresponding operation type for the action +""" +enum OperationType { + """ + An existing resource was accessed + """ + ACCESS + + """ + A resource performed an authentication event + """ + AUTHENTICATION + + """ + A new resource was created + """ + CREATE + + """ + An existing resource was modified + """ + MODIFY + + """ + An existing resource was removed + """ + REMOVE + + """ + An existing resource was restored + """ + RESTORE + + """ + An existing resource was transferred between multiple resources + """ + TRANSFER +} + +""" +Possible directions in which to order a list of items when provided an `orderBy` argument. +""" +enum OrderDirection { + """ + Specifies an ascending order for a given `orderBy` argument. + """ + ASC + + """ + Specifies a descending order for a given `orderBy` argument. + """ + DESC +} + +""" +Audit log entry for a org.add_billing_manager +""" +type OrgAddBillingManagerAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The email address used to invite a billing manager for the organization. + """ + invitationEmail: String + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.add_member +""" +type OrgAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The permission level of the member added to the organization. + """ + permission: OrgAddMemberAuditEntryPermission + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The permissions available to members on an Organization. +""" +enum OrgAddMemberAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + Can read and clone repositories. + """ + READ +} + +""" +Audit log entry for a org.block_user +""" +type OrgBlockUserAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The blocked user. + """ + blockedUser: User + + """ + The username of the blocked user. + """ + blockedUserName: String + + """ + The HTTP path for the blocked user. + """ + blockedUserResourcePath: URI + + """ + The HTTP URL for the blocked user. + """ + blockedUserUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.config.disable_collaborators_only event. +""" +type OrgConfigDisableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.config.enable_collaborators_only event. +""" +type OrgConfigEnableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.create event. +""" +type OrgCreateAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The billing plan for the Organization. + """ + billingPlan: OrgCreateAuditEntryBillingPlan + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The billing plans available for organizations. +""" +enum OrgCreateAuditEntryBillingPlan { + """ + Team Plan + """ + BUSINESS + + """ + Enterprise Cloud Plan + """ + BUSINESS_PLUS + + """ + Free Plan + """ + FREE + + """ + Tiered Per Seat Plan + """ + TIERED_PER_SEAT + + """ + Legacy Unlimited Plan + """ + UNLIMITED +} + +""" +Audit log entry for a org.disable_oauth_app_restrictions event. +""" +type OrgDisableOauthAppRestrictionsAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.disable_saml event. +""" +type OrgDisableSamlAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The SAML provider's digest algorithm URL. + """ + digestMethodUrl: URI + id: ID! + + """ + The SAML provider's issuer URL. + """ + issuerUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The SAML provider's signature algorithm URL. + """ + signatureMethodUrl: URI + + """ + The SAML provider's single sign-on URL. + """ + singleSignOnUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.disable_two_factor_requirement event. +""" +type OrgDisableTwoFactorRequirementAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.enable_oauth_app_restrictions event. +""" +type OrgEnableOauthAppRestrictionsAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.enable_saml event. +""" +type OrgEnableSamlAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The SAML provider's digest algorithm URL. + """ + digestMethodUrl: URI + id: ID! + + """ + The SAML provider's issuer URL. + """ + issuerUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The SAML provider's signature algorithm URL. + """ + signatureMethodUrl: URI + + """ + The SAML provider's single sign-on URL. + """ + singleSignOnUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.enable_two_factor_requirement event. +""" +type OrgEnableTwoFactorRequirementAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.invite_member event. +""" +type OrgInviteMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The email address of the organization invitation. + """ + email: String + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The organization invitation. + """ + organizationInvitation: OrganizationInvitation + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.invite_to_business event. +""" +type OrgInviteToBusinessAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_approved event. +""" +type OrgOauthAppAccessApprovedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth Application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth Application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth Application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_denied event. +""" +type OrgOauthAppAccessDeniedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth Application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth Application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth Application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.oauth_app_access_requested event. +""" +type OrgOauthAppAccessRequestedAuditEntry implements AuditEntry & Node & OauthApplicationAuditEntryData & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The name of the OAuth Application. + """ + oauthApplicationName: String + + """ + The HTTP path for the OAuth Application + """ + oauthApplicationResourcePath: URI + + """ + The HTTP URL for the OAuth Application + """ + oauthApplicationUrl: URI + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.remove_billing_manager event. +""" +type OrgRemoveBillingManagerAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The reason for the billing manager being removed. + """ + reason: OrgRemoveBillingManagerAuditEntryReason + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The reason a billing manager was removed from an Organization. +""" +enum OrgRemoveBillingManagerAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + SAML SSO enforcement requires an external identity + """ + SAML_SSO_ENFORCEMENT_REQUIRES_EXTERNAL_IDENTITY + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE +} + +""" +Audit log entry for a org.remove_member event. +""" +type OrgRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The types of membership the member has with the organization. + """ + membershipTypes: [OrgRemoveMemberAuditEntryMembershipType!] + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The reason for the member being removed. + """ + reason: OrgRemoveMemberAuditEntryReason + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The type of membership a user has with an Organization. +""" +enum OrgRemoveMemberAuditEntryMembershipType { + """ + Organization administrators have full access and can change several settings, + including the names of repositories that belong to the Organization and Owners + team membership. In addition, organization admins can delete the organization + and all of its repositories. + """ + ADMIN + + """ + A billing manager is a user who manages the billing settings for the Organization, such as updating payment information. + """ + BILLING_MANAGER + + """ + A direct member is a user that is a member of the Organization. + """ + DIRECT_MEMBER + + """ + An outside collaborator is a person who isn't explicitly a member of the + Organization, but who has Read, Write, or Admin permissions to one or more + repositories in the organization. + """ + OUTSIDE_COLLABORATOR + + """ + An unaffiliated collaborator is a person who is not a member of the + Organization and does not have access to any repositories in the Organization. + """ + UNAFFILIATED +} + +""" +The reason a member was removed from an Organization. +""" +enum OrgRemoveMemberAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + SAML SSO enforcement requires an external identity + """ + SAML_SSO_ENFORCEMENT_REQUIRES_EXTERNAL_IDENTITY + + """ + User was removed from organization during account recovery + """ + TWO_FACTOR_ACCOUNT_RECOVERY + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE + + """ + User account has been deleted + """ + USER_ACCOUNT_DELETED +} + +""" +Audit log entry for a org.remove_outside_collaborator event. +""" +type OrgRemoveOutsideCollaboratorAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The types of membership the outside collaborator has with the organization. + """ + membershipTypes: [OrgRemoveOutsideCollaboratorAuditEntryMembershipType!] + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The reason for the outside collaborator being removed from the Organization. + """ + reason: OrgRemoveOutsideCollaboratorAuditEntryReason + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The type of membership a user has with an Organization. +""" +enum OrgRemoveOutsideCollaboratorAuditEntryMembershipType { + """ + A billing manager is a user who manages the billing settings for the Organization, such as updating payment information. + """ + BILLING_MANAGER + + """ + An outside collaborator is a person who isn't explicitly a member of the + Organization, but who has Read, Write, or Admin permissions to one or more + repositories in the organization. + """ + OUTSIDE_COLLABORATOR + + """ + An unaffiliated collaborator is a person who is not a member of the + Organization and does not have access to any repositories in the organization. + """ + UNAFFILIATED +} + +""" +The reason an outside collaborator was removed from an Organization. +""" +enum OrgRemoveOutsideCollaboratorAuditEntryReason { + """ + SAML external identity missing + """ + SAML_EXTERNAL_IDENTITY_MISSING + + """ + The organization required 2FA of its billing managers and this user did not have 2FA enabled. + """ + TWO_FACTOR_REQUIREMENT_NON_COMPLIANCE +} + +""" +Audit log entry for a org.restore_member event. +""" +type OrgRestoreMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The number of custom email routings for the restored member. + """ + restoredCustomEmailRoutingsCount: Int + + """ + The number of issue assignments for the restored member. + """ + restoredIssueAssignmentsCount: Int + + """ + Restored organization membership objects. + """ + restoredMemberships: [OrgRestoreMemberAuditEntryMembership!] + + """ + The number of restored memberships. + """ + restoredMembershipsCount: Int + + """ + The number of repositories of the restored member. + """ + restoredRepositoriesCount: Int + + """ + The number of starred repositories for the restored member. + """ + restoredRepositoryStarsCount: Int + + """ + The number of watched repositories for the restored member. + """ + restoredRepositoryWatchesCount: Int + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Types of memberships that can be restored for an Organization member. +""" +union OrgRestoreMemberAuditEntryMembership = OrgRestoreMemberMembershipOrganizationAuditEntryData | OrgRestoreMemberMembershipRepositoryAuditEntryData | OrgRestoreMemberMembershipTeamAuditEntryData + +""" +Metadata for an organization membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipOrganizationAuditEntryData implements OrganizationAuditEntryData { + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI +} + +""" +Metadata for a repository membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipRepositoryAuditEntryData implements RepositoryAuditEntryData { + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI +} + +""" +Metadata for a team membership for org.restore_member actions +""" +type OrgRestoreMemberMembershipTeamAuditEntryData implements TeamAuditEntryData { + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI +} + +""" +Audit log entry for a org.unblock_user +""" +type OrgUnblockUserAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The user being unblocked by the organization. + """ + blockedUser: User + + """ + The username of the blocked user. + """ + blockedUserName: String + + """ + The HTTP path for the blocked user. + """ + blockedUserResourcePath: URI + + """ + The HTTP URL for the blocked user. + """ + blockedUserUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a org.update_default_repository_permission +""" +type OrgUpdateDefaultRepositoryPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The new default repository permission level for the organization. + """ + permission: OrgUpdateDefaultRepositoryPermissionAuditEntryPermission + + """ + The former default repository permission level for the organization. + """ + permissionWas: OrgUpdateDefaultRepositoryPermissionAuditEntryPermission + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The default permission a repository can have in an Organization. +""" +enum OrgUpdateDefaultRepositoryPermissionAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + No default permission value. + """ + NONE + + """ + Can read and clone repositories. + """ + READ + + """ + Can read, clone and push to repositories. + """ + WRITE +} + +""" +Audit log entry for a org.update_member event. +""" +type OrgUpdateMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The new member permission level for the organization. + """ + permission: OrgUpdateMemberAuditEntryPermission + + """ + The former member permission level for the organization. + """ + permissionWas: OrgUpdateMemberAuditEntryPermission + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The permissions available to members on an Organization. +""" +enum OrgUpdateMemberAuditEntryPermission { + """ + Can read, clone, push, and add collaborators to repositories. + """ + ADMIN + + """ + Can read and clone repositories. + """ + READ +} + +""" +Audit log entry for a org.update_member_repository_creation_permission event. +""" +type OrgUpdateMemberRepositoryCreationPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + Can members create repositories in the organization. + """ + canCreateRepositories: Boolean + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The permission for visibility level of repositories for this organization. + """ + visibility: OrgUpdateMemberRepositoryCreationPermissionAuditEntryVisibility +} + +""" +The permissions available for repository creation on an Organization. +""" +enum OrgUpdateMemberRepositoryCreationPermissionAuditEntryVisibility { + """ + All organization members are restricted from creating any repositories. + """ + ALL + + """ + All organization members are restricted from creating internal repositories. + """ + INTERNAL + + """ + All organization members are allowed to create any repositories. + """ + NONE + + """ + All organization members are restricted from creating private repositories. + """ + PRIVATE + + """ + All organization members are restricted from creating private or internal repositories. + """ + PRIVATE_INTERNAL + + """ + All organization members are restricted from creating public repositories. + """ + PUBLIC + + """ + All organization members are restricted from creating public or internal repositories. + """ + PUBLIC_INTERNAL + + """ + All organization members are restricted from creating public or private repositories. + """ + PUBLIC_PRIVATE +} + +""" +Audit log entry for a org.update_member_repository_invitation_permission event. +""" +type OrgUpdateMemberRepositoryInvitationPermissionAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + Can outside collaborators be invited to repositories in the organization. + """ + canInviteOutsideCollaboratorsToRepositories: Boolean + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +An account on GitHub, with one or more owners, that has repositories, members and teams. +""" +type Organization implements Actor & MemberStatusable & Node & PackageOwner & ProfileOwner & ProjectOwner & RepositoryOwner & Sponsorable & UniformResourceLocatable { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + Audit log entries of the organization + """ + auditLog( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned audit log entries. + """ + orderBy: AuditLogOrder = {field: CREATED_AT, direction: DESC} + + """ + The query string to filter audit entries + """ + query: String + ): OrganizationAuditEntryConnection! + + """ + A URL pointing to the organization's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The organization's public profile description. + """ + description: String + + """ + The organization's public profile description rendered to HTML. + """ + descriptionHTML: String + + """ + A list of domains owned by the organization. + """ + domains( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter by if the domain is verified. + """ + isVerified: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for verifiable domains returned. + """ + orderBy: VerifiableDomainOrder = {field: DOMAIN, direction: ASC} + ): VerifiableDomainConnection + + """ + The organization's public email. + """ + email: String + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + id: ID! + + """ + The interaction ability settings for this organization. + """ + interactionAbility: RepositoryInteractionAbility + + """ + The setting value for whether the organization has an IP allow list enabled. + """ + ipAllowListEnabledSetting: IpAllowListEnabledSettingValue! + + """ + The IP addresses that are allowed to access resources owned by the organization. + """ + ipAllowListEntries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for IP allow list entries returned. + """ + orderBy: IpAllowListEntryOrder = {field: ALLOW_LIST_VALUE, direction: ASC} + ): IpAllowListEntryConnection! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Whether the organization has verified its profile email and website, always false on Enterprise. + """ + isVerified: Boolean! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The organization's public profile location. + """ + location: String + + """ + The organization's login name. + """ + login: String! + + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! + + """ + A list of users who are members of this organization. + """ + membersWithRole( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationMemberConnection! + + """ + The organization's public profile name. + """ + name: String + + """ + The HTTP path creating a new team + """ + newTeamResourcePath: URI! + + """ + The HTTP URL creating a new team + """ + newTeamUrl: URI! + + """ + The billing email for the organization. + """ + organizationBillingEmail: String + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + A list of users who have been invited to join this organization. + """ + pendingMembers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing organization's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing organization's projects + """ + projectsUrl: URI! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + When true the organization requires all members, billing managers, and outside + collaborators to enable two-factor authentication. + """ + requiresTwoFactorAuthentication: Boolean + + """ + The HTTP path for this organization. + """ + resourcePath: URI! + + """ + The Organization's SAML identity providers + """ + samlIdentityProvider: OrganizationIdentityProvider + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + This object's sponsorships as the maintainer. + """ + sponsorshipsAsMaintainer( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + This object's sponsorships as the sponsor. + """ + sponsorshipsAsSponsor( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Find an organization's team by its slug. + """ + team( + """ + The name or slug of the team to find. + """ + slug: String! + ): Team + + """ + A list of teams in this organization. + """ + teams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + If true, filters teams that are mapped to an LDAP Group (Enterprise only) + """ + ldapMapped: Boolean + + """ + Ordering options for teams returned from the connection + """ + orderBy: TeamOrder + + """ + If non-null, filters teams according to privacy + """ + privacy: TeamPrivacy + + """ + If non-null, filters teams with query on team name and team slug + """ + query: String + + """ + If non-null, filters teams according to whether the viewer is an admin or member on team + """ + role: TeamRole + + """ + If true, restrict to only root teams + """ + rootTeamsOnly: Boolean = false + + """ + User logins to filter by + """ + userLogins: [String!] + ): TeamConnection! + + """ + The HTTP path listing organization's teams + """ + teamsResourcePath: URI! + + """ + The HTTP URL listing organization's teams + """ + teamsUrl: URI! + + """ + The organization's Twitter username. + """ + twitterUsername: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this organization. + """ + url: URI! + + """ + Organization is adminable by the viewer. + """ + viewerCanAdminister: Boolean! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + + """ + Viewer can create repositories on this organization + """ + viewerCanCreateRepositories: Boolean! + + """ + Viewer can create teams on this organization. + """ + viewerCanCreateTeams: Boolean! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + Viewer is an active member of this organization. + """ + viewerIsAMember: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! + + """ + The organization's public profile URL. + """ + websiteUrl: URI +} + +""" +An audit entry in an organization audit log. +""" +union OrganizationAuditEntry = MembersCanDeleteReposClearAuditEntry | MembersCanDeleteReposDisableAuditEntry | MembersCanDeleteReposEnableAuditEntry | OauthApplicationCreateAuditEntry | OrgAddBillingManagerAuditEntry | OrgAddMemberAuditEntry | OrgBlockUserAuditEntry | OrgConfigDisableCollaboratorsOnlyAuditEntry | OrgConfigEnableCollaboratorsOnlyAuditEntry | OrgCreateAuditEntry | OrgDisableOauthAppRestrictionsAuditEntry | OrgDisableSamlAuditEntry | OrgDisableTwoFactorRequirementAuditEntry | OrgEnableOauthAppRestrictionsAuditEntry | OrgEnableSamlAuditEntry | OrgEnableTwoFactorRequirementAuditEntry | OrgInviteMemberAuditEntry | OrgInviteToBusinessAuditEntry | OrgOauthAppAccessApprovedAuditEntry | OrgOauthAppAccessDeniedAuditEntry | OrgOauthAppAccessRequestedAuditEntry | OrgRemoveBillingManagerAuditEntry | OrgRemoveMemberAuditEntry | OrgRemoveOutsideCollaboratorAuditEntry | OrgRestoreMemberAuditEntry | OrgUnblockUserAuditEntry | OrgUpdateDefaultRepositoryPermissionAuditEntry | OrgUpdateMemberAuditEntry | OrgUpdateMemberRepositoryCreationPermissionAuditEntry | OrgUpdateMemberRepositoryInvitationPermissionAuditEntry | PrivateRepositoryForkingDisableAuditEntry | PrivateRepositoryForkingEnableAuditEntry | RepoAccessAuditEntry | RepoAddMemberAuditEntry | RepoAddTopicAuditEntry | RepoArchivedAuditEntry | RepoChangeMergeSettingAuditEntry | RepoConfigDisableAnonymousGitAccessAuditEntry | RepoConfigDisableCollaboratorsOnlyAuditEntry | RepoConfigDisableContributorsOnlyAuditEntry | RepoConfigDisableSockpuppetDisallowedAuditEntry | RepoConfigEnableAnonymousGitAccessAuditEntry | RepoConfigEnableCollaboratorsOnlyAuditEntry | RepoConfigEnableContributorsOnlyAuditEntry | RepoConfigEnableSockpuppetDisallowedAuditEntry | RepoConfigLockAnonymousGitAccessAuditEntry | RepoConfigUnlockAnonymousGitAccessAuditEntry | RepoCreateAuditEntry | RepoDestroyAuditEntry | RepoRemoveMemberAuditEntry | RepoRemoveTopicAuditEntry | RepositoryVisibilityChangeDisableAuditEntry | RepositoryVisibilityChangeEnableAuditEntry | TeamAddMemberAuditEntry | TeamAddRepositoryAuditEntry | TeamChangeParentTeamAuditEntry | TeamRemoveMemberAuditEntry | TeamRemoveRepositoryAuditEntry + +""" +The connection type for OrganizationAuditEntry. +""" +type OrganizationAuditEntryConnection { + """ + A list of edges. + """ + edges: [OrganizationAuditEntryEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationAuditEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Metadata for an audit entry with action org.* +""" +interface OrganizationAuditEntryData { + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI +} + +""" +An edge in a connection. +""" +type OrganizationAuditEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationAuditEntry +} + +""" +The connection type for Organization. +""" +type OrganizationConnection { + """ + A list of edges. + """ + edges: [OrganizationEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization +} + +""" +An Identity Provider configured to provision SAML and SCIM identities for Organizations +""" +type OrganizationIdentityProvider implements Node { + """ + The digest algorithm used to sign SAML requests for the Identity Provider. + """ + digestMethod: URI + + """ + External Identities provisioned by this Identity Provider + """ + externalIdentities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ExternalIdentityConnection! + id: ID! + + """ + The x509 certificate used by the Identity Provider to sign assertions and responses. + """ + idpCertificate: X509Certificate + + """ + The Issuer Entity ID for the SAML Identity Provider + """ + issuer: String + + """ + Organization this Identity Provider belongs to + """ + organization: Organization + + """ + The signature algorithm used to sign SAML requests for the Identity Provider. + """ + signatureMethod: URI + + """ + The URL endpoint for the Identity Provider's SAML SSO. + """ + ssoUrl: URI +} + +""" +An Invitation for a user to an organization. +""" +type OrganizationInvitation implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The email address of the user invited to the organization. + """ + email: String + id: ID! + + """ + The type of invitation that was sent (e.g. email, user). + """ + invitationType: OrganizationInvitationType! + + """ + The user who was invited to the organization. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User! + + """ + The organization the invite is for + """ + organization: Organization! + + """ + The user's pending role in the organization (e.g. member, owner). + """ + role: OrganizationInvitationRole! +} + +""" +The connection type for OrganizationInvitation. +""" +type OrganizationInvitationConnection { + """ + A list of edges. + """ + edges: [OrganizationInvitationEdge] + + """ + A list of nodes. + """ + nodes: [OrganizationInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: OrganizationInvitation +} + +""" +The possible organization invitation roles. +""" +enum OrganizationInvitationRole { + """ + The user is invited to be an admin of the organization. + """ + ADMIN + + """ + The user is invited to be a billing manager of the organization. + """ + BILLING_MANAGER + + """ + The user is invited to be a direct member of the organization. + """ + DIRECT_MEMBER + + """ + The user's previous role will be reinstated. + """ + REINSTATE +} + +""" +The possible organization invitation types. +""" +enum OrganizationInvitationType { + """ + The invitation was to an email address. + """ + EMAIL + + """ + The invitation was to an existing user. + """ + USER +} + +""" +The connection type for User. +""" +type OrganizationMemberConnection { + """ + A list of edges. + """ + edges: [OrganizationMemberEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user within an organization. +""" +type OrganizationMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + Whether the organization member has two factor enabled or not. Returns null if information is not available to viewer. + """ + hasTwoFactorEnabled: Boolean + + """ + The item at the end of the edge. + """ + node: User + + """ + The role this user has in the organization. + """ + role: OrganizationMemberRole +} + +""" +The possible roles within an organization for its members. +""" +enum OrganizationMemberRole { + """ + The user is an administrator of the organization. + """ + ADMIN + + """ + The user is a member of the organization. + """ + MEMBER +} + +""" +The possible values for the members can create repositories setting on an organization. +""" +enum OrganizationMembersCanCreateRepositoriesSettingValue { + """ + Members will be able to create public and private repositories. + """ + ALL + + """ + Members will not be able to create public or private repositories. + """ + DISABLED + + """ + Members will be able to create only private repositories. + """ + PRIVATE +} + +""" +Ordering options for organization connections. +""" +input OrganizationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order organizations by. + """ + field: OrganizationOrderField! +} + +""" +Properties by which organization connections can be ordered. +""" +enum OrganizationOrderField { + """ + Order organizations by creation time + """ + CREATED_AT + + """ + Order organizations by login + """ + LOGIN +} + +""" +An organization teams hovercard context +""" +type OrganizationTeamsHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Teams in this organization the user is a member of that are relevant + """ + relevantTeams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + The path for the full team list for this user + """ + teamsResourcePath: URI! + + """ + The URL for the full team list for this user + """ + teamsUrl: URI! + + """ + The total number of teams the user is on in the organization + """ + totalTeamCount: Int! +} + +""" +An organization list hovercard context +""" +type OrganizationsHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Organizations this user is a member of that are relevant + """ + relevantOrganizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationConnection! + + """ + The total number of organizations this user is in + """ + totalOrganizationCount: Int! +} + +""" +Information for an uploaded package. +""" +type Package implements Node { + id: ID! + + """ + Find the latest version for the package. + """ + latestVersion: PackageVersion + + """ + Identifies the name of the package. + """ + name: String! + + """ + Identifies the type of the package. + """ + packageType: PackageType! + + """ + The repository this package belongs to. + """ + repository: Repository + + """ + Statistics about package activity. + """ + statistics: PackageStatistics + + """ + Find package version by version string. + """ + version( + """ + The package version. + """ + version: String! + ): PackageVersion + + """ + list of versions for this package + """ + versions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering of the returned packages. + """ + orderBy: PackageVersionOrder = {field: CREATED_AT, direction: DESC} + ): PackageVersionConnection! +} + +""" +The connection type for Package. +""" +type PackageConnection { + """ + A list of edges. + """ + edges: [PackageEdge] + + """ + A list of nodes. + """ + nodes: [Package] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Package +} + +""" +A file in a package version. +""" +type PackageFile implements Node { + id: ID! + + """ + MD5 hash of the file. + """ + md5: String + + """ + Name of the file. + """ + name: String! + + """ + The package version this file belongs to. + """ + packageVersion: PackageVersion + + """ + SHA1 hash of the file. + """ + sha1: String + + """ + SHA256 hash of the file. + """ + sha256: String + + """ + Size of the file in bytes. + """ + size: Int + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + URL to download the asset. + """ + url: URI +} + +""" +The connection type for PackageFile. +""" +type PackageFileConnection { + """ + A list of edges. + """ + edges: [PackageFileEdge] + + """ + A list of nodes. + """ + nodes: [PackageFile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageFileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PackageFile +} + +""" +Ways in which lists of package files can be ordered upon return. +""" +input PackageFileOrder { + """ + The direction in which to order package files by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order package files by. + """ + field: PackageFileOrderField +} + +""" +Properties by which package file connections can be ordered. +""" +enum PackageFileOrderField { + """ + Order package files by creation time + """ + CREATED_AT +} + +""" +Ways in which lists of packages can be ordered upon return. +""" +input PackageOrder { + """ + The direction in which to order packages by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order packages by. + """ + field: PackageOrderField +} + +""" +Properties by which package connections can be ordered. +""" +enum PackageOrderField { + """ + Order packages by creation time + """ + CREATED_AT +} + +""" +Represents an owner of a package. +""" +interface PackageOwner { + id: ID! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! +} + +""" +Represents a object that contains package activity statistics such as downloads. +""" +type PackageStatistics { + """ + Number of times the package was downloaded since it was created. + """ + downloadsTotalCount: Int! +} + +""" +A version tag contains the mapping between a tag name and a version. +""" +type PackageTag implements Node { + id: ID! + + """ + Identifies the tag name of the version. + """ + name: String! + + """ + Version that the tag is associated with. + """ + version: PackageVersion +} + +""" +The possible types of a package. +""" +enum PackageType { + """ + A debian package. + """ + DEBIAN + + """ + A docker image. + """ + DOCKER + + """ + A maven package. + """ + MAVEN + + """ + An npm package. + """ + NPM + + """ + A nuget package. + """ + NUGET + + """ + A python package. + """ + PYPI + + """ + A rubygems package. + """ + RUBYGEMS +} + +""" +Information about a specific package version. +""" +type PackageVersion implements Node { + """ + List of files associated with this package version + """ + files( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering of the returned package files. + """ + orderBy: PackageFileOrder = {field: CREATED_AT, direction: ASC} + ): PackageFileConnection! + id: ID! + + """ + The package associated with this version. + """ + package: Package + + """ + The platform this version was built for. + """ + platform: String + + """ + Whether or not this version is a pre-release. + """ + preRelease: Boolean! + + """ + The README of this package version. + """ + readme: String + + """ + The release associated with this package version. + """ + release: Release + + """ + Statistics about package activity. + """ + statistics: PackageVersionStatistics + + """ + The package version summary. + """ + summary: String + + """ + The version string. + """ + version: String! +} + +""" +The connection type for PackageVersion. +""" +type PackageVersionConnection { + """ + A list of edges. + """ + edges: [PackageVersionEdge] + + """ + A list of nodes. + """ + nodes: [PackageVersion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PackageVersionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PackageVersion +} + +""" +Ways in which lists of package versions can be ordered upon return. +""" +input PackageVersionOrder { + """ + The direction in which to order package versions by the specified field. + """ + direction: OrderDirection + + """ + The field in which to order package versions by. + """ + field: PackageVersionOrderField +} + +""" +Properties by which package version connections can be ordered. +""" +enum PackageVersionOrderField { + """ + Order package versions by creation time + """ + CREATED_AT +} + +""" +Represents a object that contains package version activity statistics such as downloads. +""" +type PackageVersionStatistics { + """ + Number of times the package was downloaded since it was created. + """ + downloadsTotalCount: Int! +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String + + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String +} + +""" +Types that can grant permissions on a repository to a user +""" +union PermissionGranter = Organization | Repository | Team + +""" +A level of permission and source for a user's access to a repository. +""" +type PermissionSource { + """ + The organization the repository belongs to. + """ + organization: Organization! + + """ + The level of access this source has granted to the user. + """ + permission: DefaultRepositoryPermissionField! + + """ + The source of this permission. + """ + source: PermissionGranter! +} + +""" +Autogenerated input type of PinIssue +""" +input PinIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to be pinned + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of PinIssue +""" +type PinIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was pinned + """ + issue: Issue +} + +""" +Types that can be pinned to a profile page. +""" +union PinnableItem = Gist | Repository + +""" +The connection type for PinnableItem. +""" +type PinnableItemConnection { + """ + A list of edges. + """ + edges: [PinnableItemEdge] + + """ + A list of nodes. + """ + nodes: [PinnableItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnableItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnableItem +} + +""" +Represents items that can be pinned to a profile page or dashboard. +""" +enum PinnableItemType { + """ + A gist. + """ + GIST + + """ + An issue. + """ + ISSUE + + """ + An organization. + """ + ORGANIZATION + + """ + A project. + """ + PROJECT + + """ + A pull request. + """ + PULL_REQUEST + + """ + A repository. + """ + REPOSITORY + + """ + A team. + """ + TEAM + + """ + A user. + """ + USER +} + +""" +Represents a 'pinned' event on a given issue or pull request. +""" +type PinnedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +A Pinned Issue is a issue pinned to a repository's index page. +""" +type PinnedIssue implements Node @preview(toggledBy: "elektra-preview") { + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The issue that was pinned. + """ + issue: Issue! + + """ + The actor that pinned this issue. + """ + pinnedBy: Actor! + + """ + The repository that this issue was pinned to. + """ + repository: Repository! +} + +""" +The connection type for PinnedIssue. +""" +type PinnedIssueConnection @preview(toggledBy: "elektra-preview") { + """ + A list of edges. + """ + edges: [PinnedIssueEdge] + + """ + A list of nodes. + """ + nodes: [PinnedIssue] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PinnedIssueEdge @preview(toggledBy: "elektra-preview") { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PinnedIssue +} + +""" +An ISO-8601 encoded UTC date string with millisecond precision. +""" +scalar PreciseDateTime + +""" +Audit log entry for a private_repository_forking.disable event. +""" +type PrivateRepositoryForkingDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a private_repository_forking.enable event. +""" +type PrivateRepositoryForkingEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +A curatable list of repositories relating to a repository owner, which defaults +to showing the most popular repositories they own. +""" +type ProfileItemShowcase { + """ + Whether or not the owner has pinned any repositories or gists. + """ + hasPinnedItems: Boolean! + + """ + The repositories and gists in the showcase. If the profile owner has any + pinned items, those will be returned. Otherwise, the profile owner's popular + repositories will be returned. + """ + items( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnableItemConnection! +} + +""" +Represents any entity on GitHub that has a profile page. +""" +interface ProfileOwner { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + The public profile email. + """ + email: String + id: ID! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The public profile location. + """ + location: String + + """ + The username used to login. + """ + login: String! + + """ + The public profile name. + """ + name: String + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + The public profile website URL. + """ + websiteUrl: URI +} + +""" +Projects manage issues, pull requests and notes within a project owner. +""" +type Project implements Closable & Node & Updatable { + """ + The project's description body. + """ + body: String + + """ + The projects description body rendered to HTML. + """ + bodyHTML: HTML! + + """ + `true` if the object is closed (definition of closed may depend on type) + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + List of columns in the project + """ + columns( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectColumnConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who originally created the project. + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project's name. + """ + name: String! + + """ + The project's number. + """ + number: Int! + + """ + The project's owner. Currently limited to repositories, organizations, and users. + """ + owner: ProjectOwner! + + """ + List of pending cards in this project + """ + pendingCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + Project progress details. + """ + progress: ProjectProgress! + + """ + The HTTP path for this project + """ + resourcePath: URI! + + """ + Whether the project is open or closed. + """ + state: ProjectState! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this project + """ + url: URI! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! +} + +""" +A card in a project. +""" +type ProjectCard implements Node { + """ + The project column this card is associated under. A card may only belong to one + project column at a time. The column field will be null if the card is created + in a pending state and has yet to be associated with a column. Once cards are + associated with a column, they will not become pending in the future. + """ + column: ProjectColumn + + """ + The card content item + """ + content: ProjectCardItem + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this card + """ + creator: Actor + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Whether the card is archived + """ + isArchived: Boolean! + + """ + The card note + """ + note: String + + """ + The project that contains this card. + """ + project: Project! + + """ + The HTTP path for this card + """ + resourcePath: URI! + + """ + The state of ProjectCard + """ + state: ProjectCardState + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this card + """ + url: URI! +} + +""" +The possible archived states of a project card. +""" +enum ProjectCardArchivedState { + """ + A project card that is archived + """ + ARCHIVED + + """ + A project card that is not archived + """ + NOT_ARCHIVED +} + +""" +The connection type for ProjectCard. +""" +type ProjectCardConnection { + """ + A list of edges. + """ + edges: [ProjectCardEdge] + + """ + A list of nodes. + """ + nodes: [ProjectCard] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectCardEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectCard +} + +""" +An issue or PR and its owning repository to be used in a project card. +""" +input ProjectCardImport { + """ + The issue or pull request number. + """ + number: Int! + + """ + Repository name with owner (owner/repository). + """ + repository: String! +} + +""" +Types that can be inside Project Cards. +""" +union ProjectCardItem = Issue | PullRequest + +""" +Various content states of a ProjectCard +""" +enum ProjectCardState { + """ + The card has content only. + """ + CONTENT_ONLY + + """ + The card has a note only. + """ + NOTE_ONLY + + """ + The card is redacted. + """ + REDACTED +} + +""" +A column inside a project. +""" +type ProjectColumn implements Node { + """ + List of cards in the column + """ + cards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The project column's name. + """ + name: String! + + """ + The project that contains this column. + """ + project: Project! + + """ + The semantic purpose of the column + """ + purpose: ProjectColumnPurpose + + """ + The HTTP path for this project column + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this project column + """ + url: URI! +} + +""" +The connection type for ProjectColumn. +""" +type ProjectColumnConnection { + """ + A list of edges. + """ + edges: [ProjectColumnEdge] + + """ + A list of nodes. + """ + nodes: [ProjectColumn] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectColumnEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ProjectColumn +} + +""" +A project column and a list of its issues and PRs. +""" +input ProjectColumnImport { + """ + The name of the column. + """ + columnName: String! + + """ + A list of issues and pull requests in the column. + """ + issues: [ProjectCardImport!] + + """ + The position of the column, starting from 0. + """ + position: Int! +} + +""" +The semantic purpose of the column - todo, in progress, or done. +""" +enum ProjectColumnPurpose { + """ + The column contains cards which are complete + """ + DONE + + """ + The column contains cards which are currently being worked on + """ + IN_PROGRESS + + """ + The column contains cards still to be worked on + """ + TODO +} + +""" +A list of projects associated with the owner. +""" +type ProjectConnection { + """ + A list of edges. + """ + edges: [ProjectEdge] + + """ + A list of nodes. + """ + nodes: [Project] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProjectEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Project +} + +""" +Ways in which lists of projects can be ordered upon return. +""" +input ProjectOrder { + """ + The direction in which to order projects by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order projects by. + """ + field: ProjectOrderField! +} + +""" +Properties by which project connections can be ordered. +""" +enum ProjectOrderField { + """ + Order projects by creation time + """ + CREATED_AT + + """ + Order projects by name + """ + NAME + + """ + Order projects by update time + """ + UPDATED_AT +} + +""" +Represents an owner of a Project. +""" +interface ProjectOwner { + id: ID! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing owners projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing owners projects + """ + projectsUrl: URI! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! +} + +""" +Project progress stats. +""" +type ProjectProgress { + """ + The number of done cards. + """ + doneCount: Int! + + """ + The percentage of done cards. + """ + donePercentage: Float! + + """ + Whether progress tracking is enabled and cards with purpose exist for this project + """ + enabled: Boolean! + + """ + The number of in-progress cards. + """ + inProgressCount: Int! + + """ + The percentage of in-progress cards. + """ + inProgressPercentage: Float! + + """ + The number of to do cards. + """ + todoCount: Int! + + """ + The percentage of to do cards. + """ + todoPercentage: Float! +} + +""" +State of the project; either 'open' or 'closed' +""" +enum ProjectState { + """ + The project is closed. + """ + CLOSED + + """ + The project is open. + """ + OPEN +} + +""" +GitHub-provided templates for Projects +""" +enum ProjectTemplate { + """ + Create a board with v2 triggers to automatically move cards across To do, In progress and Done columns. + """ + AUTOMATED_KANBAN_V2 + + """ + Create a board with triggers to automatically move cards across columns with review automation. + """ + AUTOMATED_REVIEWS_KANBAN + + """ + Create a board with columns for To do, In progress and Done. + """ + BASIC_KANBAN + + """ + Create a board to triage and prioritize bugs with To do, priority, and Done columns. + """ + BUG_TRIAGE +} + +""" +A user's public key. +""" +type PublicKey implements Node { + """ + The last time this authorization was used to perform an action. Values will be null for keys not owned by the user. + """ + accessedAt: DateTime + + """ + Identifies the date and time when the key was created. Keys created before + March 5th, 2014 have inaccurate values. Values will be null for keys not owned by the user. + """ + createdAt: DateTime + + """ + The fingerprint for this PublicKey. + """ + fingerprint: String! + id: ID! + + """ + Whether this PublicKey is read-only or not. Values will be null for keys not owned by the user. + """ + isReadOnly: Boolean + + """ + The public key string. + """ + key: String! + + """ + Identifies the date and time when the key was updated. Keys created before + March 5th, 2014 may have inaccurate values. Values will be null for keys not + owned by the user. + """ + updatedAt: DateTime +} + +""" +The connection type for PublicKey. +""" +type PublicKeyConnection { + """ + A list of edges. + """ + edges: [PublicKeyEdge] + + """ + A list of nodes. + """ + nodes: [PublicKey] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PublicKeyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PublicKey +} + +""" +A repository pull request. +""" +type PullRequest implements Assignable & Closable & Comment & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + Reason that the conversation was locked. + """ + activeLockReason: LockReason + + """ + The number of additions in this pull request. + """ + additions: Int! + + """ + A list of Users assigned to this object. + """ + assignees( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Identifies the base Ref associated with the pull request. + """ + baseRef: Ref + + """ + Identifies the name of the base Ref associated with the pull request, even if the ref has been deleted. + """ + baseRefName: String! + + """ + Identifies the oid of the base ref associated with the pull request, even if the ref has been deleted. + """ + baseRefOid: GitObjectID! + + """ + The repository associated with this pull request's base Ref. + """ + baseRepository: Repository + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Whether or not the pull request is rebaseable. + """ + canBeRebased: Boolean! @preview(toggledBy: "merge-info-preview") + + """ + The number of changed files in this pull request. + """ + changedFiles: Int! + + """ + The HTTP path for the checks of this pull request. + """ + checksResourcePath: URI! + + """ + The HTTP URL for the checks of this pull request. + """ + checksUrl: URI! + + """ + `true` if the pull request is closed + """ + closed: Boolean! + + """ + Identifies the date and time when the object was closed. + """ + closedAt: DateTime + + """ + A list of comments associated with the pull request. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + A list of commits present in this pull request's head branch not present in the base branch. + """ + commits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestCommitConnection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The number of deletions in this pull request. + """ + deletions: Int! + + """ + The actor who edited this pull request's body. + """ + editor: Actor + + """ + Lists the files changed within this pull request. + """ + files( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestChangedFileConnection + + """ + Identifies the head Ref associated with the pull request. + """ + headRef: Ref + + """ + Identifies the name of the head Ref associated with the pull request, even if the ref has been deleted. + """ + headRefName: String! + + """ + Identifies the oid of the head ref associated with the pull request, even if the ref has been deleted. + """ + headRefOid: GitObjectID! + + """ + The repository associated with this pull request's head Ref. + """ + headRepository: Repository + + """ + The owner of the repository associated with this pull request's head Ref. + """ + headRepositoryOwner: RepositoryOwner + + """ + The hovercard information for this issue + """ + hovercard( + """ + Whether or not to include notification contexts + """ + includeNotificationContexts: Boolean = true + ): Hovercard! + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The head and base repositories are different. + """ + isCrossRepository: Boolean! + + """ + Identifies if the pull request is a draft. + """ + isDraft: Boolean! + + """ + Is this pull request read by the viewer + """ + isReadByViewer: Boolean + + """ + A list of labels associated with the object. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + ): LabelConnection + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + A list of latest reviews per user associated with the pull request. + """ + latestOpinionatedReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Only return reviews from user who have write access to the repository + """ + writersOnly: Boolean = false + ): PullRequestReviewConnection + + """ + A list of latest reviews per user associated with the pull request that are not also pending review. + """ + latestReviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewConnection + + """ + `true` if the pull request is locked + """ + locked: Boolean! + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean! + + """ + The commit that was created when this pull request was merged. + """ + mergeCommit: Commit + + """ + Detailed information about the current pull request merge state status. + """ + mergeStateStatus: MergeStateStatus! @preview(toggledBy: "merge-info-preview") + + """ + Whether or not the pull request can be merged based on the existence of merge conflicts. + """ + mergeable: MergeableState! + + """ + Whether or not the pull request was merged. + """ + merged: Boolean! + + """ + The date and time that the pull request was merged. + """ + mergedAt: DateTime + + """ + The actor who merged the pull request. + """ + mergedBy: Actor + + """ + Identifies the milestone associated with the pull request. + """ + milestone: Milestone + + """ + Identifies the pull request number. + """ + number: Int! + + """ + A list of Users that are participating in the Pull Request conversation. + """ + participants( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! + + """ + The permalink to the pull request. + """ + permalink: URI! + + """ + The commit that GitHub automatically generated to test if this pull request + could be merged. This field will not return a value if the pull request is + merged, or if the test merge commit is still being generated. See the + `mergeable` field for more details on the mergeability of the pull request. + """ + potentialMergeCommit: Commit + + """ + List of project cards associated with this pull request. + """ + projectCards( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + A list of archived states to filter the cards by + """ + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ProjectCardConnection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path for this pull request. + """ + resourcePath: URI! + + """ + The HTTP path for reverting this pull request. + """ + revertResourcePath: URI! + + """ + The HTTP URL for reverting this pull request. + """ + revertUrl: URI! + + """ + The current status of this pull request with respect to code review. + """ + reviewDecision: PullRequestReviewDecision + + """ + A list of review requests associated with the pull request. + """ + reviewRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReviewRequestConnection + + """ + The list of all review threads for this pull request. + """ + reviewThreads( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewThreadConnection! + + """ + A list of reviews associated with the pull request. + """ + reviews( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Filter by author of the review. + """ + author: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A list of states to filter the reviews. + """ + states: [PullRequestReviewState!] + ): PullRequestReviewConnection + + """ + Identifies the state of the pull request. + """ + state: PullRequestState! + + """ + A list of reviewer suggestions based on commit history and past review comments. + """ + suggestedReviewers: [SuggestedReviewer]! + + """ + A list of events, comments, commits, etc. associated with the pull request. + """ + timeline( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows filtering timeline events by a `since` timestamp. + """ + since: DateTime + ): PullRequestTimelineConnection! @deprecated(reason: "`timeline` will be removed Use PullRequest.timelineItems instead. Removal on 2020-10-01 UTC.") + + """ + A list of events, comments, commits, etc. associated with the pull request. + """ + timelineItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter timeline items by type. + """ + itemTypes: [PullRequestTimelineItemsItemType!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter timeline items by a `since` timestamp. + """ + since: DateTime + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestTimelineItemsConnection! + + """ + Identifies the pull request title. + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this pull request. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Whether or not the viewer can apply suggestion. + """ + viewerCanApplySuggestion: Boolean! + + """ + Check if the viewer can restore the deleted head ref. + """ + viewerCanDeleteHeadRef: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + The merge body text for the viewer and method. + """ + viewerMergeBodyText( + """ + The merge method for the message. + """ + mergeType: PullRequestMergeMethod + ): String! + + """ + The merge headline text for the viewer and method. + """ + viewerMergeHeadlineText( + """ + The merge method for the message. + """ + mergeType: PullRequestMergeMethod + ): String! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +A file changed in a pull request. +""" +type PullRequestChangedFile { + """ + The number of additions to the file. + """ + additions: Int! + + """ + The number of deletions to the file. + """ + deletions: Int! + + """ + The path of the file. + """ + path: String! + + """ + The state of the file for the viewer. + """ + viewerViewedState: FileViewedState! +} + +""" +The connection type for PullRequestChangedFile. +""" +type PullRequestChangedFileConnection { + """ + A list of edges. + """ + edges: [PullRequestChangedFileEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestChangedFile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestChangedFileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestChangedFile +} + +""" +Represents a Git commit part of a pull request. +""" +type PullRequestCommit implements Node & UniformResourceLocatable { + """ + The Git commit object + """ + commit: Commit! + id: ID! + + """ + The pull request this commit belongs to + """ + pullRequest: PullRequest! + + """ + The HTTP path for this pull request commit + """ + resourcePath: URI! + + """ + The HTTP URL for this pull request commit + """ + url: URI! +} + +""" +Represents a commit comment thread part of a pull request. +""" +type PullRequestCommitCommentThread implements Node & RepositoryNode { + """ + The comments that exist in this thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The commit the comments were made on. + """ + commit: Commit! + id: ID! + + """ + The file the comments were made on. + """ + path: String + + """ + The position in the diff for the commit that the comment was made on. + """ + position: Int + + """ + The pull request this commit comment thread belongs to + """ + pullRequest: PullRequest! + + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +The connection type for PullRequestCommit. +""" +type PullRequestCommitConnection { + """ + A list of edges. + """ + edges: [PullRequestCommitEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestCommit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestCommitEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestCommit +} + +""" +The connection type for PullRequest. +""" +type PullRequestConnection { + """ + A list of edges. + """ + edges: [PullRequestEdge] + + """ + A list of nodes. + """ + nodes: [PullRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates pull requests opened by a user within one repository. +""" +type PullRequestContributionsByRepository { + """ + The pull request contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestContributionConnection! + + """ + The repository in which the pull requests were opened. + """ + repository: Repository! +} + +""" +An edge in a connection. +""" +type PullRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequest +} + +""" +Represents available types of methods to use when merging a pull request. +""" +enum PullRequestMergeMethod { + """ + Add all commits from the head branch to the base branch with a merge commit. + """ + MERGE + + """ + Add all commits from the head branch onto the base branch individually. + """ + REBASE + + """ + Combine all commits from the head branch into a single commit in the base branch. + """ + SQUASH +} + +""" +Ways in which lists of issues can be ordered upon return. +""" +input PullRequestOrder { + """ + The direction in which to order pull requests by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order pull requests by. + """ + field: PullRequestOrderField! +} + +""" +Properties by which pull_requests connections can be ordered. +""" +enum PullRequestOrderField { + """ + Order pull_requests by creation time + """ + CREATED_AT + + """ + Order pull_requests by update time + """ + UPDATED_AT +} + +""" +A review object for a given pull request. +""" +type PullRequestReview implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + Indicates whether the author of this review has push access to the repository. + """ + authorCanPushToRepository: Boolean! + + """ + Identifies the pull request review body. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body of this review rendered as plain text. + """ + bodyText: String! + + """ + A list of review comments for the current pull request review. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PullRequestReviewCommentConnection! + + """ + Identifies the commit associated with this pull request review. + """ + commit: Commit + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + A list of teams that this review was made on behalf of. + """ + onBehalfOf( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + Identifies the pull request associated with this pull request review. + """ + pullRequest: PullRequest! + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this PullRequestReview. + """ + resourcePath: URI! + + """ + Identifies the current state of the pull request review. + """ + state: PullRequestReviewState! + + """ + Identifies when the Pull Request Review was submitted + """ + submittedAt: DateTime + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this PullRequestReview. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +A review comment associated with a given repository pull request. +""" +type PullRequestReviewComment implements Comment & Deletable & Minimizable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the subject of the comment. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The comment body of this review comment. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The comment body of this review comment rendered as plain text. + """ + bodyText: String! + + """ + Identifies the commit associated with the comment. + """ + commit: Commit + + """ + Identifies when the comment was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The diff hunk to which the comment applies. + """ + diffHunk: String! + + """ + Identifies when the comment was created in a draft state. + """ + draftedAt: DateTime! + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Returns whether or not a comment has been minimized. + """ + isMinimized: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Returns why the comment was minimized. + """ + minimizedReason: String + + """ + Identifies the original commit associated with the comment. + """ + originalCommit: Commit + + """ + The original line index in the diff to which the comment applies. + """ + originalPosition: Int! + + """ + Identifies when the comment body is outdated + """ + outdated: Boolean! + + """ + The path to which the comment applies. + """ + path: String! + + """ + The line index in the diff to which the comment applies. + """ + position: Int + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + The pull request associated with this review comment. + """ + pullRequest: PullRequest! + + """ + The pull request review associated with this review comment. + """ + pullRequestReview: PullRequestReview + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The comment this is a reply to. + """ + replyTo: PullRequestReviewComment + + """ + The repository associated with this node. + """ + repository: Repository! + + """ + The HTTP path permalink for this review comment. + """ + resourcePath: URI! + + """ + Identifies the state of the comment. + """ + state: PullRequestReviewCommentState! + + """ + Identifies when the comment was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL permalink for this review comment. + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Check if the current viewer can minimize this object. + """ + viewerCanMinimize: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for PullRequestReviewComment. +""" +type PullRequestReviewCommentConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewCommentEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReviewComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestReviewCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReviewComment +} + +""" +The possible states of a pull request review comment. +""" +enum PullRequestReviewCommentState { + """ + A comment that is part of a pending review + """ + PENDING + + """ + A comment that is part of a submitted review + """ + SUBMITTED +} + +""" +The connection type for PullRequestReview. +""" +type PullRequestReviewConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReview] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +This aggregates pull request reviews made by a user within one repository. +""" +type PullRequestReviewContributionsByRepository { + """ + The pull request review contributions. + """ + contributions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for contributions returned from the connection. + """ + orderBy: ContributionOrder = {direction: DESC} + ): CreatedPullRequestReviewContributionConnection! + + """ + The repository in which the pull request reviews were made. + """ + repository: Repository! +} + +""" +The review status of a pull request. +""" +enum PullRequestReviewDecision { + """ + The pull request has received an approving review. + """ + APPROVED + + """ + Changes have been requested on the pull request. + """ + CHANGES_REQUESTED + + """ + A review is required before the pull request can be merged. + """ + REVIEW_REQUIRED +} + +""" +An edge in a connection. +""" +type PullRequestReviewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReview +} + +""" +The possible events to perform on a pull request review. +""" +enum PullRequestReviewEvent { + """ + Submit feedback and approve merging these changes. + """ + APPROVE + + """ + Submit general feedback without explicit approval. + """ + COMMENT + + """ + Dismiss review so it now longer effects merging. + """ + DISMISS + + """ + Submit feedback that must be addressed before merging. + """ + REQUEST_CHANGES +} + +""" +The possible states of a pull request review. +""" +enum PullRequestReviewState { + """ + A review allowing the pull request to merge. + """ + APPROVED + + """ + A review blocking the pull request from merging. + """ + CHANGES_REQUESTED + + """ + An informational review. + """ + COMMENTED + + """ + A review that has been dismissed. + """ + DISMISSED + + """ + A review that has not yet been submitted. + """ + PENDING +} + +""" +A threaded list of comments for a given pull request. +""" +type PullRequestReviewThread implements Node { + """ + A list of pull request comments associated with the thread. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Skips the first _n_ elements in the list. + """ + skip: Int + ): PullRequestReviewCommentConnection! + + """ + The side of the diff on which this thread was placed. + """ + diffSide: DiffSide! + id: ID! + + """ + Whether or not the thread has been collapsed (outdated or resolved) + """ + isCollapsed: Boolean! + + """ + Indicates whether this thread was outdated by newer changes. + """ + isOutdated: Boolean! + + """ + Whether this thread has been resolved + """ + isResolved: Boolean! + + """ + The line in the file to which this thread refers + """ + line: Int + + """ + The original line in the file to which this thread refers. + """ + originalLine: Int + + """ + The original start line in the file to which this thread refers (multi-line only). + """ + originalStartLine: Int + + """ + Identifies the file path of this thread. + """ + path: String! + + """ + Identifies the pull request associated with this thread. + """ + pullRequest: PullRequest! + + """ + Identifies the repository associated with this thread. + """ + repository: Repository! + + """ + The user who resolved this thread + """ + resolvedBy: User + + """ + The side of the diff that the first line of the thread starts on (multi-line only) + """ + startDiffSide: DiffSide + + """ + The start line in the file to which this thread refers (multi-line only) + """ + startLine: Int + + """ + Indicates whether the current viewer can reply to this thread. + """ + viewerCanReply: Boolean! + + """ + Whether or not the viewer can resolve this thread + """ + viewerCanResolve: Boolean! + + """ + Whether or not the viewer can unresolve this thread + """ + viewerCanUnresolve: Boolean! +} + +""" +Review comment threads for a pull request review. +""" +type PullRequestReviewThreadConnection { + """ + A list of edges. + """ + edges: [PullRequestReviewThreadEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestReviewThread] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PullRequestReviewThreadEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestReviewThread +} + +""" +Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. +""" +type PullRequestRevisionMarker { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The last commit the viewer has seen. + """ + lastSeenCommit: Commit! + + """ + The pull request to which the marker belongs. + """ + pullRequest: PullRequest! +} + +""" +The possible states of a pull request. +""" +enum PullRequestState { + """ + A pull request that has been closed without being merged. + """ + CLOSED + + """ + A pull request that has been closed by being merged. + """ + MERGED + + """ + A pull request that is still open. + """ + OPEN +} + +""" +The connection type for PullRequestTimelineItem. +""" +type PullRequestTimelineConnection { + """ + A list of edges. + """ + edges: [PullRequestTimelineItemEdge] + + """ + A list of nodes. + """ + nodes: [PullRequestTimelineItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An item in a pull request timeline +""" +union PullRequestTimelineItem = AssignedEvent | BaseRefDeletedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit | CommitCommentThread | CrossReferencedEvent | DemilestonedEvent | DeployedEvent | DeploymentEnvironmentChangedEvent | HeadRefDeletedEvent | HeadRefForcePushedEvent | HeadRefRestoredEvent | IssueComment | LabeledEvent | LockedEvent | MergedEvent | MilestonedEvent | PullRequestReview | PullRequestReviewComment | PullRequestReviewThread | ReferencedEvent | RenamedTitleEvent | ReopenedEvent | ReviewDismissedEvent | ReviewRequestRemovedEvent | ReviewRequestedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent | UserBlockedEvent + +""" +An edge in a connection. +""" +type PullRequestTimelineItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestTimelineItem +} + +""" +An item in a pull request timeline +""" +union PullRequestTimelineItems = AddedToProjectEvent | AssignedEvent | AutoMergeDisabledEvent | AutoMergeEnabledEvent | AutoRebaseEnabledEvent | AutoSquashEnabledEvent | AutomaticBaseChangeFailedEvent | AutomaticBaseChangeSucceededEvent | BaseRefChangedEvent | BaseRefDeletedEvent | BaseRefForcePushedEvent | ClosedEvent | CommentDeletedEvent | ConnectedEvent | ConvertToDraftEvent | ConvertedNoteToIssueEvent | CrossReferencedEvent | DemilestonedEvent | DeployedEvent | DeploymentEnvironmentChangedEvent | DisconnectedEvent | HeadRefDeletedEvent | HeadRefForcePushedEvent | HeadRefRestoredEvent | IssueComment | LabeledEvent | LockedEvent | MarkedAsDuplicateEvent | MentionedEvent | MergedEvent | MilestonedEvent | MovedColumnsInProjectEvent | PinnedEvent | PullRequestCommit | PullRequestCommitCommentThread | PullRequestReview | PullRequestReviewThread | PullRequestRevisionMarker | ReadyForReviewEvent | ReferencedEvent | RemovedFromProjectEvent | RenamedTitleEvent | ReopenedEvent | ReviewDismissedEvent | ReviewRequestRemovedEvent | ReviewRequestedEvent | SubscribedEvent | TransferredEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnmarkedAsDuplicateEvent | UnpinnedEvent | UnsubscribedEvent | UserBlockedEvent + +""" +The connection type for PullRequestTimelineItems. +""" +type PullRequestTimelineItemsConnection { + """ + A list of edges. + """ + edges: [PullRequestTimelineItemsEdge] + + """ + Identifies the count of items after applying `before` and `after` filters. + """ + filteredCount: Int! + + """ + A list of nodes. + """ + nodes: [PullRequestTimelineItems] + + """ + Identifies the count of items after applying `before`/`after` filters and `first`/`last`/`skip` slicing. + """ + pageCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Identifies the date and time when the timeline was last updated. + """ + updatedAt: DateTime! +} + +""" +An edge in a connection. +""" +type PullRequestTimelineItemsEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PullRequestTimelineItems +} + +""" +The possible item types found in a timeline. +""" +enum PullRequestTimelineItemsItemType { + """ + Represents a 'added_to_project' event on a given issue or pull request. + """ + ADDED_TO_PROJECT_EVENT + + """ + Represents an 'assigned' event on any assignable object. + """ + ASSIGNED_EVENT + + """ + Represents a 'automatic_base_change_failed' event on a given pull request. + """ + AUTOMATIC_BASE_CHANGE_FAILED_EVENT + + """ + Represents a 'automatic_base_change_succeeded' event on a given pull request. + """ + AUTOMATIC_BASE_CHANGE_SUCCEEDED_EVENT + + """ + Represents a 'auto_merge_disabled' event on a given pull request. + """ + AUTO_MERGE_DISABLED_EVENT + + """ + Represents a 'auto_merge_enabled' event on a given pull request. + """ + AUTO_MERGE_ENABLED_EVENT + + """ + Represents a 'auto_rebase_enabled' event on a given pull request. + """ + AUTO_REBASE_ENABLED_EVENT + + """ + Represents a 'auto_squash_enabled' event on a given pull request. + """ + AUTO_SQUASH_ENABLED_EVENT + + """ + Represents a 'base_ref_changed' event on a given issue or pull request. + """ + BASE_REF_CHANGED_EVENT + + """ + Represents a 'base_ref_deleted' event on a given pull request. + """ + BASE_REF_DELETED_EVENT + + """ + Represents a 'base_ref_force_pushed' event on a given pull request. + """ + BASE_REF_FORCE_PUSHED_EVENT + + """ + Represents a 'closed' event on any `Closable`. + """ + CLOSED_EVENT + + """ + Represents a 'comment_deleted' event on a given issue or pull request. + """ + COMMENT_DELETED_EVENT + + """ + Represents a 'connected' event on a given issue or pull request. + """ + CONNECTED_EVENT + + """ + Represents a 'converted_note_to_issue' event on a given issue or pull request. + """ + CONVERTED_NOTE_TO_ISSUE_EVENT + + """ + Represents a 'convert_to_draft' event on a given pull request. + """ + CONVERT_TO_DRAFT_EVENT + + """ + Represents a mention made by one issue or pull request to another. + """ + CROSS_REFERENCED_EVENT + + """ + Represents a 'demilestoned' event on a given issue or pull request. + """ + DEMILESTONED_EVENT + + """ + Represents a 'deployed' event on a given pull request. + """ + DEPLOYED_EVENT + + """ + Represents a 'deployment_environment_changed' event on a given pull request. + """ + DEPLOYMENT_ENVIRONMENT_CHANGED_EVENT + + """ + Represents a 'disconnected' event on a given issue or pull request. + """ + DISCONNECTED_EVENT + + """ + Represents a 'head_ref_deleted' event on a given pull request. + """ + HEAD_REF_DELETED_EVENT + + """ + Represents a 'head_ref_force_pushed' event on a given pull request. + """ + HEAD_REF_FORCE_PUSHED_EVENT + + """ + Represents a 'head_ref_restored' event on a given pull request. + """ + HEAD_REF_RESTORED_EVENT + + """ + Represents a comment on an Issue. + """ + ISSUE_COMMENT + + """ + Represents a 'labeled' event on a given issue or pull request. + """ + LABELED_EVENT + + """ + Represents a 'locked' event on a given issue or pull request. + """ + LOCKED_EVENT + + """ + Represents a 'marked_as_duplicate' event on a given issue or pull request. + """ + MARKED_AS_DUPLICATE_EVENT + + """ + Represents a 'mentioned' event on a given issue or pull request. + """ + MENTIONED_EVENT + + """ + Represents a 'merged' event on a given pull request. + """ + MERGED_EVENT + + """ + Represents a 'milestoned' event on a given issue or pull request. + """ + MILESTONED_EVENT + + """ + Represents a 'moved_columns_in_project' event on a given issue or pull request. + """ + MOVED_COLUMNS_IN_PROJECT_EVENT + + """ + Represents a 'pinned' event on a given issue or pull request. + """ + PINNED_EVENT + + """ + Represents a Git commit part of a pull request. + """ + PULL_REQUEST_COMMIT + + """ + Represents a commit comment thread part of a pull request. + """ + PULL_REQUEST_COMMIT_COMMENT_THREAD + + """ + A review object for a given pull request. + """ + PULL_REQUEST_REVIEW + + """ + A threaded list of comments for a given pull request. + """ + PULL_REQUEST_REVIEW_THREAD + + """ + Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. + """ + PULL_REQUEST_REVISION_MARKER + + """ + Represents a 'ready_for_review' event on a given pull request. + """ + READY_FOR_REVIEW_EVENT + + """ + Represents a 'referenced' event on a given `ReferencedSubject`. + """ + REFERENCED_EVENT + + """ + Represents a 'removed_from_project' event on a given issue or pull request. + """ + REMOVED_FROM_PROJECT_EVENT + + """ + Represents a 'renamed' event on a given issue or pull request + """ + RENAMED_TITLE_EVENT + + """ + Represents a 'reopened' event on any `Closable`. + """ + REOPENED_EVENT + + """ + Represents a 'review_dismissed' event on a given issue or pull request. + """ + REVIEW_DISMISSED_EVENT + + """ + Represents an 'review_requested' event on a given pull request. + """ + REVIEW_REQUESTED_EVENT + + """ + Represents an 'review_request_removed' event on a given pull request. + """ + REVIEW_REQUEST_REMOVED_EVENT + + """ + Represents a 'subscribed' event on a given `Subscribable`. + """ + SUBSCRIBED_EVENT + + """ + Represents a 'transferred' event on a given issue or pull request. + """ + TRANSFERRED_EVENT + + """ + Represents an 'unassigned' event on any assignable object. + """ + UNASSIGNED_EVENT + + """ + Represents an 'unlabeled' event on a given issue or pull request. + """ + UNLABELED_EVENT + + """ + Represents an 'unlocked' event on a given issue or pull request. + """ + UNLOCKED_EVENT + + """ + Represents an 'unmarked_as_duplicate' event on a given issue or pull request. + """ + UNMARKED_AS_DUPLICATE_EVENT + + """ + Represents an 'unpinned' event on a given issue or pull request. + """ + UNPINNED_EVENT + + """ + Represents an 'unsubscribed' event on a given `Subscribable`. + """ + UNSUBSCRIBED_EVENT + + """ + Represents a 'user_blocked' event on a given user. + """ + USER_BLOCKED_EVENT +} + +""" +The possible target states when updating a pull request. +""" +enum PullRequestUpdateState { + """ + A pull request that has been closed without being merged. + """ + CLOSED + + """ + A pull request that is still open. + """ + OPEN +} + +""" +A Git push. +""" +type Push implements Node { + id: ID! + + """ + The SHA after the push + """ + nextSha: GitObjectID + + """ + The permalink for this push. + """ + permalink: URI! + + """ + The SHA before the push + """ + previousSha: GitObjectID + + """ + The user who pushed + """ + pusher: User! + + """ + The repository that was pushed to + """ + repository: Repository! +} + +""" +A team, user or app who has the ability to push to a protected branch. +""" +type PushAllowance implements Node { + """ + The actor that can push. + """ + actor: PushAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user or team. + """ + branchProtectionRule: BranchProtectionRule + id: ID! +} + +""" +Types that can be an actor. +""" +union PushAllowanceActor = App | Team | User + +""" +The connection type for PushAllowance. +""" +type PushAllowanceConnection { + """ + A list of edges. + """ + edges: [PushAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [PushAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PushAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: PushAllowance +} + +""" +The query root of GitHub's GraphQL interface. +""" +type Query { + """ + Look up a code of conduct by its key + """ + codeOfConduct( + """ + The code of conduct's key + """ + key: String! + ): CodeOfConduct + + """ + Look up a code of conduct by its key + """ + codesOfConduct: [CodeOfConduct] + + """ + Look up an enterprise by URL slug. + """ + enterprise( + """ + The enterprise invitation token. + """ + invitationToken: String + + """ + The enterprise URL slug. + """ + slug: String! + ): Enterprise + + """ + Look up a pending enterprise administrator invitation by invitee, enterprise and role. + """ + enterpriseAdministratorInvitation( + """ + The slug of the enterprise the user was invited to join. + """ + enterpriseSlug: String! + + """ + The role for the business member invitation. + """ + role: EnterpriseAdministratorRole! + + """ + The login of the user invited to join the business. + """ + userLogin: String! + ): EnterpriseAdministratorInvitation + + """ + Look up a pending enterprise administrator invitation by invitation token. + """ + enterpriseAdministratorInvitationByToken( + """ + The invitation token sent with the invitation email. + """ + invitationToken: String! + ): EnterpriseAdministratorInvitation + + """ + Look up an open source license by its key + """ + license( + """ + The license's downcased SPDX ID + """ + key: String! + ): License + + """ + Return a list of known open source licenses + """ + licenses: [License]! + + """ + Get alphabetically sorted list of Marketplace categories + """ + marketplaceCategories( + """ + Exclude categories with no listings. + """ + excludeEmpty: Boolean + + """ + Returns top level categories only, excluding any subcategories. + """ + excludeSubcategories: Boolean + + """ + Return only the specified categories. + """ + includeCategories: [String!] + ): [MarketplaceCategory!]! + + """ + Look up a Marketplace category by its slug. + """ + marketplaceCategory( + """ + The URL slug of the category. + """ + slug: String! + + """ + Also check topic aliases for the category slug + """ + useTopicAliases: Boolean + ): MarketplaceCategory + + """ + Look up a single Marketplace listing + """ + marketplaceListing( + """ + Select the listing that matches this slug. It's the short name of the listing used in its URL. + """ + slug: String! + ): MarketplaceListing + + """ + Look up Marketplace listings + """ + marketplaceListings( + """ + Select listings that can be administered by the specified user. + """ + adminId: ID + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Select listings visible to the viewer even if they are not approved. If omitted or + false, only approved listings will be returned. + """ + allStates: Boolean + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Select only listings with the given category. + """ + categorySlug: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Select listings for products owned by the specified organization. + """ + organizationId: ID + + """ + Select only listings where the primary category matches the given category slug. + """ + primaryCategoryOnly: Boolean = false + + """ + Select the listings with these slugs, if they are visible to the viewer. + """ + slugs: [String] + + """ + Also check topic aliases for the category slug + """ + useTopicAliases: Boolean + + """ + Select listings to which user has admin access. If omitted, listings visible to the + viewer are returned. + """ + viewerCanAdmin: Boolean + + """ + Select only listings that offer a free trial. + """ + withFreeTrialsOnly: Boolean = false + ): MarketplaceListingConnection! + + """ + Return information about the GitHub instance + """ + meta: GitHubMetadata! + + """ + Fetches an object given its ID. + """ + node( + """ + ID of the object. + """ + id: ID! + ): Node + + """ + Lookup nodes by a list of IDs. + """ + nodes( + """ + The list of node IDs. + """ + ids: [ID!]! + ): [Node]! + + """ + Lookup a organization by login. + """ + organization( + """ + The organization's login. + """ + login: String! + ): Organization + + """ + The client's rate limit information. + """ + rateLimit( + """ + If true, calculate the cost for the query without evaluating it + """ + dryRun: Boolean = false + ): RateLimit + + """ + Hack to workaround https://github.com/facebook/relay/issues/112 re-exposing the root query object + """ + relay: Query! + + """ + Lookup a given repository by the owner and repository name. + """ + repository( + """ + The name of the repository + """ + name: String! + + """ + The login field of a user or organization + """ + owner: String! + ): Repository + + """ + Lookup a repository owner (ie. either a User or an Organization) by login. + """ + repositoryOwner( + """ + The username to lookup the owner by. + """ + login: String! + ): RepositoryOwner + + """ + Lookup resource by a URL. + """ + resource( + """ + The URL. + """ + url: URI! + ): UniformResourceLocatable + + """ + Perform a search across resources. + """ + search( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The search string to look for. + """ + query: String! + + """ + The types of search items to search within. + """ + type: SearchType! + ): SearchResultItemConnection! + + """ + GitHub Security Advisories + """ + securityAdvisories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filter advisories by identifier, e.g. GHSA or CVE. + """ + identifier: SecurityAdvisoryIdentifierFilter + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityAdvisoryOrder = {field: UPDATED_AT, direction: DESC} + + """ + Filter advisories to those published since a time in the past. + """ + publishedSince: DateTime + + """ + Filter advisories to those updated since a time in the past. + """ + updatedSince: DateTime + ): SecurityAdvisoryConnection! + + """ + Fetch a Security Advisory by its GHSA ID + """ + securityAdvisory( + """ + GitHub Security Advisory ID. + """ + ghsaId: String! + ): SecurityAdvisory + + """ + Software Vulnerabilities documented by GitHub Security Advisories + """ + securityVulnerabilities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + An ecosystem to filter vulnerabilities by. + """ + ecosystem: SecurityAdvisoryEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC} + + """ + A package name to filter vulnerabilities by. + """ + package: String + + """ + A list of severities to filter vulnerabilities by. + """ + severities: [SecurityAdvisorySeverity!] + ): SecurityVulnerabilityConnection! + + """ + Look up a single Sponsors Listing + """ + sponsorsListing( + """ + Select the Sponsors listing which matches this slug + """ + slug: String! + ): SponsorsListing @deprecated(reason: "`Query.sponsorsListing` will be removed. Use `Sponsorable.sponsorsListing` instead. Removal on 2020-04-01 UTC.") + + """ + Look up a topic by name. + """ + topic( + """ + The topic's name. + """ + name: String! + ): Topic + + """ + Lookup a user by login. + """ + user( + """ + The user's login. + """ + login: String! + ): User + + """ + The currently authenticated user. + """ + viewer: User! +} + +""" +Represents the client's rate limit. +""" +type RateLimit { + """ + The point cost for the current query counting against the rate limit. + """ + cost: Int! + + """ + The maximum number of points the client is permitted to consume in a 60 minute window. + """ + limit: Int! + + """ + The maximum number of nodes this query may return + """ + nodeCount: Int! + + """ + The number of points remaining in the current rate limit window. + """ + remaining: Int! + + """ + The time at which the current rate limit window resets in UTC epoch seconds. + """ + resetAt: DateTime! + + """ + The number of points used in the current rate limit window. + """ + used: Int! +} + +""" +Represents a subject that can be reacted on. +""" +interface Reactable { + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! +} + +""" +The connection type for User. +""" +type ReactingUserConnection { + """ + A list of edges. + """ + edges: [ReactingUserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user that's made a reaction. +""" +type ReactingUserEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + The moment when the user made the reaction. + """ + reactedAt: DateTime! +} + +""" +An emoji reaction to a particular piece of content. +""" +type Reaction implements Node { + """ + Identifies the emoji reaction. + """ + content: ReactionContent! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The reactable piece of content + """ + reactable: Reactable! + + """ + Identifies the user who created this reaction. + """ + user: User +} + +""" +A list of reactions that have been left on the subject. +""" +type ReactionConnection { + """ + A list of edges. + """ + edges: [ReactionEdge] + + """ + A list of nodes. + """ + nodes: [Reaction] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + Whether or not the authenticated user has left a reaction on the subject. + """ + viewerHasReacted: Boolean! +} + +""" +Emojis that can be attached to Issues, Pull Requests and Comments. +""" +enum ReactionContent { + """ + Represents the `:confused:` emoji. + """ + CONFUSED + + """ + Represents the `:eyes:` emoji. + """ + EYES + + """ + Represents the `:heart:` emoji. + """ + HEART + + """ + Represents the `:hooray:` emoji. + """ + HOORAY + + """ + Represents the `:laugh:` emoji. + """ + LAUGH + + """ + Represents the `:rocket:` emoji. + """ + ROCKET + + """ + Represents the `:-1:` emoji. + """ + THUMBS_DOWN + + """ + Represents the `:+1:` emoji. + """ + THUMBS_UP +} + +""" +An edge in a connection. +""" +type ReactionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Reaction +} + +""" +A group of emoji reactions to a particular piece of content. +""" +type ReactionGroup { + """ + Identifies the emoji reaction. + """ + content: ReactionContent! + + """ + Identifies when the reaction was created. + """ + createdAt: DateTime + + """ + The subject that was reacted to. + """ + subject: Reactable! + + """ + Users who have reacted to the reaction subject with the emotion represented by this reaction group + """ + users( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ReactingUserConnection! + + """ + Whether or not the authenticated user has left a reaction on the subject. + """ + viewerHasReacted: Boolean! +} + +""" +Ways in which lists of reactions can be ordered upon return. +""" +input ReactionOrder { + """ + The direction in which to order reactions by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order reactions by. + """ + field: ReactionOrderField! +} + +""" +A list of fields that reactions can be ordered by. +""" +enum ReactionOrderField { + """ + Allows ordering a list of reactions by when they were created. + """ + CREATED_AT +} + +""" +Represents a 'ready_for_review' event on a given pull request. +""" +type ReadyForReviewEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + The HTTP path for this ready for review event. + """ + resourcePath: URI! + + """ + The HTTP URL for this ready for review event. + """ + url: URI! +} + +""" +Represents a Git reference. +""" +type Ref implements Node { + """ + A list of pull requests with this ref as the head ref. + """ + associatedPullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Branch protection rules for this ref + """ + branchProtectionRule: BranchProtectionRule + id: ID! + + """ + The ref name. + """ + name: String! + + """ + The ref's prefix, such as `refs/heads/` or `refs/tags/`. + """ + prefix: String! + + """ + Branch protection rules that are viewable by non-admins + """ + refUpdateRule: RefUpdateRule + + """ + The repository the ref belongs to. + """ + repository: Repository! + + """ + The object the ref points to. Returns null when object does not exist. + """ + target: GitObject +} + +""" +The connection type for Ref. +""" +type RefConnection { + """ + A list of edges. + """ + edges: [RefEdge] + + """ + A list of nodes. + """ + nodes: [Ref] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RefEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Ref +} + +""" +Ways in which lists of git refs can be ordered upon return. +""" +input RefOrder { + """ + The direction in which to order refs by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order refs by. + """ + field: RefOrderField! +} + +""" +Properties by which ref connections can be ordered. +""" +enum RefOrderField { + """ + Order refs by their alphanumeric name + """ + ALPHABETICAL + + """ + Order refs by underlying commit date if the ref prefix is refs/tags/ + """ + TAG_COMMIT_DATE +} + +""" +A ref update +""" +input RefUpdate @preview(toggledBy: "update-refs-preview") { + """ + The value this ref should be updated to. + """ + afterOid: GitObjectID! + + """ + The value this ref needs to point to before the update. + """ + beforeOid: GitObjectID + + """ + Force a non fast-forward update. + """ + force: Boolean = false + + """ + The fully qualified name of the ref to be update. For example `refs/heads/branch-name` + """ + name: GitRefname! +} + +""" +A ref update rules for a viewer. +""" +type RefUpdateRule { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean! + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean! + + """ + Identifies the protection rule pattern. + """ + pattern: String! + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String] + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean! + + """ + Are commits required to be signed. + """ + requiresSignatures: Boolean! + + """ + Can the viewer push to the branch + """ + viewerCanPush: Boolean! +} + +""" +Represents a 'referenced' event on a given `ReferencedSubject`. +""" +type ReferencedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the commit associated with the 'referenced' event. + """ + commit: Commit + + """ + Identifies the repository associated with the 'referenced' event. + """ + commitRepository: Repository! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Reference originated in a different repository. + """ + isCrossRepository: Boolean! + + """ + Checks if the commit message itself references the subject. Can be false in the case of a commit comment reference. + """ + isDirectReference: Boolean! + + """ + Object referenced by event. + """ + subject: ReferencedSubject! +} + +""" +Any referencable object +""" +union ReferencedSubject = Issue | PullRequest + +""" +Autogenerated input type of RegenerateEnterpriseIdentityProviderRecoveryCodes +""" +input RegenerateEnterpriseIdentityProviderRecoveryCodesInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set an identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) +} + +""" +Autogenerated return type of RegenerateEnterpriseIdentityProviderRecoveryCodes +""" +type RegenerateEnterpriseIdentityProviderRecoveryCodesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider for the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of RegenerateVerifiableDomainToken +""" +input RegenerateVerifiableDomainTokenInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to regenerate the verification token of. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of RegenerateVerifiableDomainToken +""" +type RegenerateVerifiableDomainTokenPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verification token that was generated. + """ + verificationToken: String +} + +""" +A release contains the content for a release. +""" +type Release implements Node & UniformResourceLocatable { + """ + The author of the release + """ + author: User + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the release. + """ + description: String + + """ + The description of this release rendered to HTML. + """ + descriptionHTML: HTML + id: ID! + + """ + Whether or not the release is a draft + """ + isDraft: Boolean! + + """ + Whether or not the release is the latest releast + """ + isLatest: Boolean! + + """ + Whether or not the release is a prerelease + """ + isPrerelease: Boolean! + + """ + The title of the release. + """ + name: String + + """ + Identifies the date and time when the release was created. + """ + publishedAt: DateTime + + """ + List of releases assets which are dependent on this release. + """ + releaseAssets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + A list of names to filter the assets by. + """ + name: String + ): ReleaseAssetConnection! + + """ + The HTTP path for this issue + """ + resourcePath: URI! + + """ + A description of the release, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML + + """ + The Git tag the release points to + """ + tag: Ref + + """ + The name of the release's Git tag + """ + tagName: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this issue + """ + url: URI! +} + +""" +A release asset contains the content for a release asset. +""" +type ReleaseAsset implements Node { + """ + The asset's content-type + """ + contentType: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The number of times this asset was downloaded + """ + downloadCount: Int! + + """ + Identifies the URL where you can download the release asset via the browser. + """ + downloadUrl: URI! + id: ID! + + """ + Identifies the title of the release asset. + """ + name: String! + + """ + Release that the asset is associated with + """ + release: Release + + """ + The size (in bytes) of the asset + """ + size: Int! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user that performed the upload + """ + uploadedBy: User! + + """ + Identifies the URL of the release asset. + """ + url: URI! +} + +""" +The connection type for ReleaseAsset. +""" +type ReleaseAssetConnection { + """ + A list of edges. + """ + edges: [ReleaseAssetEdge] + + """ + A list of nodes. + """ + nodes: [ReleaseAsset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReleaseAssetEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReleaseAsset +} + +""" +The connection type for Release. +""" +type ReleaseConnection { + """ + A list of edges. + """ + edges: [ReleaseEdge] + + """ + A list of nodes. + """ + nodes: [Release] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReleaseEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Release +} + +""" +Ways in which lists of releases can be ordered upon return. +""" +input ReleaseOrder { + """ + The direction in which to order releases by the specified field. + """ + direction: OrderDirection! + + """ + The field in which to order releases by. + """ + field: ReleaseOrderField! +} + +""" +Properties by which release connections can be ordered. +""" +enum ReleaseOrderField { + """ + Order releases by creation time + """ + CREATED_AT + + """ + Order releases alphabetically by name + """ + NAME +} + +""" +Autogenerated input type of RemoveAssigneesFromAssignable +""" +input RemoveAssigneesFromAssignableInput { + """ + The id of the assignable object to remove assignees from. + """ + assignableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Assignable") + + """ + The id of users to remove as assignees. + """ + assigneeIds: [ID!]! @possibleTypes(concreteTypes: ["User"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of RemoveAssigneesFromAssignable +""" +type RemoveAssigneesFromAssignablePayload { + """ + The item that was unassigned. + """ + assignable: Assignable + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of RemoveEnterpriseAdmin +""" +input RemoveEnterpriseAdminInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Enterprise ID from which to remove the administrator. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of the user to remove as an administrator. + """ + login: String! +} + +""" +Autogenerated return type of RemoveEnterpriseAdmin +""" +type RemoveEnterpriseAdminPayload { + """ + The user who was removed as an administrator. + """ + admin: User + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + A message confirming the result of removing an administrator. + """ + message: String + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseIdentityProvider +""" +input RemoveEnterpriseIdentityProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which to remove the identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) +} + +""" +Autogenerated return type of RemoveEnterpriseIdentityProvider +""" +type RemoveEnterpriseIdentityProviderPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider that was removed from the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of RemoveEnterpriseOrganization +""" +input RemoveEnterpriseOrganizationInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise from which the organization should be removed. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The ID of the organization to remove from the enterprise. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of RemoveEnterpriseOrganization +""" +type RemoveEnterpriseOrganizationPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise + + """ + The organization that was removed from the enterprise. + """ + organization: Organization + + """ + The viewer performing the mutation. + """ + viewer: User +} + +""" +Autogenerated input type of RemoveEnterpriseSupportEntitlement +""" +input RemoveEnterpriseSupportEntitlementInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a member who will lose the support entitlement. + """ + login: String! +} + +""" +Autogenerated return type of RemoveEnterpriseSupportEntitlement +""" +type RemoveEnterpriseSupportEntitlementPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of removing the support entitlement. + """ + message: String +} + +""" +Autogenerated input type of RemoveLabelsFromLabelable +""" +input RemoveLabelsFromLabelableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ids of labels to remove. + """ + labelIds: [ID!]! @possibleTypes(concreteTypes: ["Label"]) + + """ + The id of the Labelable to remove labels from. + """ + labelableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Labelable") +} + +""" +Autogenerated return type of RemoveLabelsFromLabelable +""" +type RemoveLabelsFromLabelablePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Labelable the labels were removed from. + """ + labelable: Labelable +} + +""" +Autogenerated input type of RemoveOutsideCollaborator +""" +input RemoveOutsideCollaboratorInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the organization to remove the outside collaborator from. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) + + """ + The ID of the outside collaborator to remove. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RemoveOutsideCollaborator +""" +type RemoveOutsideCollaboratorPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was removed as an outside collaborator. + """ + removedUser: User +} + +""" +Autogenerated input type of RemoveReaction +""" +input RemoveReactionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of the emoji reaction to remove. + """ + content: ReactionContent! + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["CommitComment", "Issue", "IssueComment", "PullRequest", "PullRequestReview", "PullRequestReviewComment", "TeamDiscussion", "TeamDiscussionComment"], abstractType: "Reactable") +} + +""" +Autogenerated return type of RemoveReaction +""" +type RemoveReactionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The reaction object. + """ + reaction: Reaction + + """ + The reactable subject. + """ + subject: Reactable +} + +""" +Autogenerated input type of RemoveStar +""" +input RemoveStarInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Starrable ID to unstar. + """ + starrableId: ID! @possibleTypes(concreteTypes: ["Gist", "Repository", "Topic"], abstractType: "Starrable") +} + +""" +Autogenerated return type of RemoveStar +""" +type RemoveStarPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The starrable. + """ + starrable: Starrable +} + +""" +Represents a 'removed_from_project' event on a given issue or pull request. +""" +type RemovedFromProjectEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Project referenced by event. + """ + project: Project @preview(toggledBy: "starfox-preview") + + """ + Column name referenced by this project event. + """ + projectColumnName: String! @preview(toggledBy: "starfox-preview") +} + +""" +Represents a 'renamed' event on a given issue or pull request +""" +type RenamedTitleEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the current title of the issue or pull request. + """ + currentTitle: String! + id: ID! + + """ + Identifies the previous title of the issue or pull request. + """ + previousTitle: String! + + """ + Subject that was renamed. + """ + subject: RenamedTitleSubject! +} + +""" +An object which has a renamable title +""" +union RenamedTitleSubject = Issue | PullRequest + +""" +Autogenerated input type of ReopenIssue +""" +input ReopenIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue to be opened. + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of ReopenIssue +""" +type ReopenIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was opened. + """ + issue: Issue +} + +""" +Autogenerated input type of ReopenPullRequest +""" +input ReopenPullRequestInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the pull request to be reopened. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of ReopenPullRequest +""" +type ReopenPullRequestPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that was reopened. + """ + pullRequest: PullRequest +} + +""" +Represents a 'reopened' event on any `Closable`. +""" +type ReopenedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Object that was reopened. + """ + closable: Closable! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! +} + +""" +Audit log entry for a repo.access event. +""" +type RepoAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoAccessAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoAccessAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.add_member event. +""" +type RepoAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoAddMemberAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoAddMemberAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.add_topic event. +""" +type RepoAddTopicAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TopicAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.archived event. +""" +type RepoArchivedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoArchivedAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoArchivedAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.change_merge_setting event. +""" +type RepoChangeMergeSettingAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the change was to enable (true) or disable (false) the merge type + """ + isEnabled: Boolean + + """ + The merge method affected by the change + """ + mergeType: RepoChangeMergeSettingAuditEntryMergeType + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The merge options available for pull requests to this repository. +""" +enum RepoChangeMergeSettingAuditEntryMergeType { + """ + The pull request is added to the base branch in a merge commit. + """ + MERGE + + """ + Commits from the pull request are added onto the base branch individually without a merge commit. + """ + REBASE + + """ + The pull request's commits are squashed into a single commit before they are merged to the base branch. + """ + SQUASH +} + +""" +Audit log entry for a repo.config.disable_anonymous_git_access event. +""" +type RepoConfigDisableAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.disable_collaborators_only event. +""" +type RepoConfigDisableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.disable_contributors_only event. +""" +type RepoConfigDisableContributorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.disable_sockpuppet_disallowed event. +""" +type RepoConfigDisableSockpuppetDisallowedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_anonymous_git_access event. +""" +type RepoConfigEnableAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_collaborators_only event. +""" +type RepoConfigEnableCollaboratorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_contributors_only event. +""" +type RepoConfigEnableContributorsOnlyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.enable_sockpuppet_disallowed event. +""" +type RepoConfigEnableSockpuppetDisallowedAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.lock_anonymous_git_access event. +""" +type RepoConfigLockAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.config.unlock_anonymous_git_access event. +""" +type RepoConfigUnlockAnonymousGitAccessAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repo.create event. +""" +type RepoCreateAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The name of the parent repository for this forked repository. + """ + forkParentName: String + + """ + The name of the root repository for this network. + """ + forkSourceName: String + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoCreateAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoCreateAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.destroy event. +""" +type RepoDestroyAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoDestroyAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoDestroyAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.remove_member event. +""" +type RepoRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI + + """ + The visibility of the repository + """ + visibility: RepoRemoveMemberAuditEntryVisibility +} + +""" +The privacy of a repository +""" +enum RepoRemoveMemberAuditEntryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repo.remove_topic event. +""" +type RepoRemoveTopicAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TopicAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The reasons a piece of content can be reported or minimized. +""" +enum ReportedContentClassifiers { + """ + An abusive or harassing piece of content + """ + ABUSE + + """ + A duplicated piece of content + """ + DUPLICATE + + """ + An irrelevant piece of content + """ + OFF_TOPIC + + """ + An outdated piece of content + """ + OUTDATED + + """ + The content has been resolved + """ + RESOLVED + + """ + A spammy piece of content + """ + SPAM +} + +""" +A repository contains the content for a project. +""" +type Repository implements Node & PackageOwner & ProjectOwner & RepositoryInfo & Starrable & Subscribable & UniformResourceLocatable { + """ + A list of users that can be assigned to issues in this repository. + """ + assignableUsers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login + """ + query: String + ): UserConnection! + + """ + A list of branch protection rules for this repository. + """ + branchProtectionRules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): BranchProtectionRuleConnection! + + """ + Returns the code of conduct for this repository + """ + codeOfConduct: CodeOfConduct + + """ + A list of collaborators associated with the repository. + """ + collaborators( + """ + Collaborators affiliation level with a repository. + """ + affiliation: CollaboratorAffiliation + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login + """ + query: String + ): RepositoryCollaboratorConnection + + """ + A list of commit comments associated with the repository. + """ + commitComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + Returns a list of contact links associated to the repository + """ + contactLinks: [RepositoryContactLink!] + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The Ref associated with the repository's default branch. + """ + defaultBranchRef: Ref + + """ + Whether or not branches are automatically deleted when merged in this repository. + """ + deleteBranchOnMerge: Boolean! + + """ + A list of dependency manifests contained in the repository + """ + dependencyGraphManifests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Cursor to paginate dependencies + """ + dependenciesAfter: String + + """ + Number of dependencies to fetch + """ + dependenciesFirst: Int + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Flag to scope to only manifests with dependencies + """ + withDependencies: Boolean + ): DependencyGraphManifestConnection @preview(toggledBy: "hawkgirl-preview") + + """ + A list of deploy keys that are on this repository. + """ + deployKeys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DeployKeyConnection! + + """ + Deployments associated with the repository + """ + deployments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Environments to list deployments for + """ + environments: [String!] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for deployments returned from the connection. + """ + orderBy: DeploymentOrder = {field: CREATED_AT, direction: ASC} + ): DeploymentConnection! + + """ + The description of the repository. + """ + description: String + + """ + The description of the repository rendered to HTML. + """ + descriptionHTML: HTML! + + """ + The number of kilobytes this repository occupies on disk. + """ + diskUsage: Int + + """ + Returns how many forks there are of this repository in the whole network. + """ + forkCount: Int! + + """ + A list of direct forked repositories. + """ + forks( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + The funding links for this repository + """ + fundingLinks: [FundingLink!]! + + """ + Indicates if the repository has issues feature enabled. + """ + hasIssuesEnabled: Boolean! + + """ + Indicates if the repository has the Projects feature enabled. + """ + hasProjectsEnabled: Boolean! + + """ + Indicates if the repository has wiki feature enabled. + """ + hasWikiEnabled: Boolean! + + """ + The repository's URL. + """ + homepageUrl: URI + id: ID! + + """ + The interaction ability settings for this repository. + """ + interactionAbility: RepositoryInteractionAbility + + """ + Indicates if the repository is unmaintained. + """ + isArchived: Boolean! + + """ + Returns true if blank issue creation is allowed + """ + isBlankIssuesEnabled: Boolean! + + """ + Returns whether or not this repository disabled. + """ + isDisabled: Boolean! + + """ + Returns whether or not this repository is empty. + """ + isEmpty: Boolean! + + """ + Identifies if the repository is a fork. + """ + isFork: Boolean! + + """ + Indicates if a repository is either owned by an organization, or is a private fork of an organization repository. + """ + isInOrganization: Boolean! + + """ + Indicates if the repository has been locked or not. + """ + isLocked: Boolean! + + """ + Identifies if the repository is a mirror. + """ + isMirror: Boolean! + + """ + Identifies if the repository is private. + """ + isPrivate: Boolean! + + """ + Returns true if this repository has a security policy + """ + isSecurityPolicyEnabled: Boolean + + """ + Identifies if the repository is a template that can be used to generate new repositories. + """ + isTemplate: Boolean! + + """ + Is this repository a user configuration repository? + """ + isUserConfigurationRepository: Boolean! + + """ + Returns a single issue from the current repository by number. + """ + issue( + """ + The number for the issue to be returned. + """ + number: Int! + ): Issue + + """ + Returns a single issue-like object from the current repository by number. + """ + issueOrPullRequest( + """ + The number for the issue to be returned. + """ + number: Int! + ): IssueOrPullRequest + + """ + Returns a list of issue templates associated to the repository + """ + issueTemplates: [IssueTemplate!] + + """ + A list of issues that have been opened in the repository. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Returns a single label by name + """ + label( + """ + Label name + """ + name: String! + ): Label + + """ + A list of labels associated with the repository. + """ + labels( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for labels returned from the connection. + """ + orderBy: LabelOrder = {field: CREATED_AT, direction: ASC} + + """ + If provided, searches labels by name and description. + """ + query: String + ): LabelConnection + + """ + A list containing a breakdown of the language composition of the repository. + """ + languages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: LanguageOrder + ): LanguageConnection + + """ + Get the latest release for the repository if one exists. + """ + latestRelease: Release + + """ + The license associated with the repository + """ + licenseInfo: License + + """ + The reason the repository has been locked. + """ + lockReason: RepositoryLockReason + + """ + A list of Users that can be mentioned in the context of the repository. + """ + mentionableUsers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filters users with query on user name and login + """ + query: String + ): UserConnection! + + """ + Whether or not PRs are merged with a merge commit on this repository. + """ + mergeCommitAllowed: Boolean! + + """ + Returns a single milestone from the current repository by number. + """ + milestone( + """ + The number for the milestone to be returned. + """ + number: Int! + ): Milestone + + """ + A list of milestones associated with the repository. + """ + milestones( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for milestones. + """ + orderBy: MilestoneOrder + + """ + Filters milestones with a query on the title + """ + query: String + + """ + Filter by the state of the milestones. + """ + states: [MilestoneState!] + ): MilestoneConnection + + """ + The repository's original mirror URL. + """ + mirrorUrl: URI + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + A Git object in the repository + """ + object( + """ + A Git revision expression suitable for rev-parse + """ + expression: String + + """ + The Git object ID + """ + oid: GitObjectID + ): GitObject + + """ + The image used to represent this repository in Open Graph data. + """ + openGraphImageUrl: URI! + + """ + The User owner of the repository. + """ + owner: RepositoryOwner! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + The repository parent, if this is a fork. + """ + parent: Repository + + """ + A list of pinned issues for this repository. + """ + pinnedIssues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PinnedIssueConnection @preview(toggledBy: "elektra-preview") + + """ + The primary language of the repository's code. + """ + primaryLanguage: Language + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing the repository's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing the repository's projects + """ + projectsUrl: URI! + + """ + Returns a single pull request from the current repository by number. + """ + pullRequest( + """ + The number for the pull request to be returned. + """ + number: Int! + ): PullRequest + + """ + A list of pull requests that have been opened in the repository. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + Identifies when the repository was last pushed to. + """ + pushedAt: DateTime + + """ + Whether or not rebase-merging is enabled on this repository. + """ + rebaseMergeAllowed: Boolean! + + """ + Fetch a given ref from the repository + """ + ref( + """ + The ref to retrieve. Fully qualified matches are checked in order + (`refs/heads/master`) before falling back onto checks for short name matches (`master`). + """ + qualifiedName: String! + ): Ref + + """ + Fetch a list of refs from the repository + """ + refs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + DEPRECATED: use orderBy. The ordering direction. + """ + direction: OrderDirection + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for refs returned from the connection. + """ + orderBy: RefOrder + + """ + Filters refs with query on name + """ + query: String + + """ + A ref name prefix like `refs/heads/`, `refs/tags/`, etc. + """ + refPrefix: String! + ): RefConnection + + """ + Lookup a single release given various criteria. + """ + release( + """ + The name of the Tag the Release was created from + """ + tagName: String! + ): Release + + """ + List of releases which are dependent on this repository. + """ + releases( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: ReleaseOrder + ): ReleaseConnection! + + """ + A list of applied repository-topic associations for this repository. + """ + repositoryTopics( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryTopicConnection! + + """ + The HTTP path for this repository + """ + resourcePath: URI! + + """ + The security policy URL. + """ + securityPolicyUrl: URI + + """ + A description of the repository, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML! + + """ + Whether or not squash-merging is enabled on this repository. + """ + squashMergeAllowed: Boolean! + + """ + The SSH URL to clone this repository + """ + sshUrl: GitSSHRemote! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a list of all submodules in this repository parsed from the + .gitmodules file as of the default branch's HEAD commit. + """ + submodules( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SubmoduleConnection! + + """ + Temporary authentication token for cloning this repository. + """ + tempCloneToken: String + + """ + The repository from which this repository was generated, if any. + """ + templateRepository: Repository + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this repository + """ + url: URI! + + """ + Whether this repository has a custom image to use with Open Graph as opposed to being represented by the owner's avatar. + """ + usesCustomOpenGraphImage: Boolean! + + """ + Indicates whether the viewer has admin permissions on this repository. + """ + viewerCanAdminister: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Indicates whether the viewer can update the topics of this repository. + """ + viewerCanUpdateTopics: Boolean! + + """ + The last commit email for the viewer. + """ + viewerDefaultCommitEmail: String + + """ + The last used merge method by the viewer or the default for the repository. + """ + viewerDefaultMergeMethod: PullRequestMergeMethod! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! + + """ + The users permission level on the repository. Will return null if authenticated as an GitHub App. + """ + viewerPermission: RepositoryPermission + + """ + A list of emails this viewer can commit with. + """ + viewerPossibleCommitEmails: [String!] + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState + + """ + A list of vulnerability alerts that are on this repository. + """ + vulnerabilityAlerts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RepositoryVulnerabilityAlertConnection + + """ + A list of users watching the repository. + """ + watchers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection! +} + +""" +The affiliation of a user to a repository +""" +enum RepositoryAffiliation { + """ + Repositories that the user has been added to as a collaborator. + """ + COLLABORATOR + + """ + Repositories that the user has access to through being a member of an + organization. This includes every repository on every team that the user is on. + """ + ORGANIZATION_MEMBER + + """ + Repositories that are owned by the authenticated user. + """ + OWNER +} + +""" +Metadata for an audit entry with action repo.* +""" +interface RepositoryAuditEntryData { + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI +} + +""" +The connection type for User. +""" +type RepositoryCollaboratorConnection { + """ + A list of edges. + """ + edges: [RepositoryCollaboratorEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user who is a collaborator of a repository. +""" +type RepositoryCollaboratorEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + The permission the user has on the repository. + """ + permission: RepositoryPermission! + + """ + A list of sources for the user's access to the repository. + """ + permissionSources: [PermissionSource!] +} + +""" +A list of repositories owned by the subject. +""" +type RepositoryConnection { + """ + A list of edges. + """ + edges: [RepositoryEdge] + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! + + """ + The total size in kilobytes of all repositories in the connection. + """ + totalDiskUsage: Int! +} + +""" +A repository contact link. +""" +type RepositoryContactLink { + """ + The contact link purpose. + """ + about: String! + + """ + The contact link name. + """ + name: String! + + """ + The contact link URL. + """ + url: URI! +} + +""" +The reason a repository is listed as 'contributed'. +""" +enum RepositoryContributionType { + """ + Created a commit + """ + COMMIT + + """ + Created an issue + """ + ISSUE + + """ + Created a pull request + """ + PULL_REQUEST + + """ + Reviewed a pull request + """ + PULL_REQUEST_REVIEW + + """ + Created the repository + """ + REPOSITORY +} + +""" +An edge in a connection. +""" +type RepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Repository +} + +""" +A subset of repository info. +""" +interface RepositoryInfo { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the repository. + """ + description: String + + """ + The description of the repository rendered to HTML. + """ + descriptionHTML: HTML! + + """ + Returns how many forks there are of this repository in the whole network. + """ + forkCount: Int! + + """ + Indicates if the repository has issues feature enabled. + """ + hasIssuesEnabled: Boolean! + + """ + Indicates if the repository has the Projects feature enabled. + """ + hasProjectsEnabled: Boolean! + + """ + Indicates if the repository has wiki feature enabled. + """ + hasWikiEnabled: Boolean! + + """ + The repository's URL. + """ + homepageUrl: URI + + """ + Indicates if the repository is unmaintained. + """ + isArchived: Boolean! + + """ + Identifies if the repository is a fork. + """ + isFork: Boolean! + + """ + Indicates if a repository is either owned by an organization, or is a private fork of an organization repository. + """ + isInOrganization: Boolean! + + """ + Indicates if the repository has been locked or not. + """ + isLocked: Boolean! + + """ + Identifies if the repository is a mirror. + """ + isMirror: Boolean! + + """ + Identifies if the repository is private. + """ + isPrivate: Boolean! + + """ + Identifies if the repository is a template that can be used to generate new repositories. + """ + isTemplate: Boolean! + + """ + The license associated with the repository + """ + licenseInfo: License + + """ + The reason the repository has been locked. + """ + lockReason: RepositoryLockReason + + """ + The repository's original mirror URL. + """ + mirrorUrl: URI + + """ + The name of the repository. + """ + name: String! + + """ + The repository's name with owner. + """ + nameWithOwner: String! + + """ + The image used to represent this repository in Open Graph data. + """ + openGraphImageUrl: URI! + + """ + The User owner of the repository. + """ + owner: RepositoryOwner! + + """ + Identifies when the repository was last pushed to. + """ + pushedAt: DateTime + + """ + The HTTP path for this repository + """ + resourcePath: URI! + + """ + A description of the repository, rendered to HTML without any links in it. + """ + shortDescriptionHTML( + """ + How many characters to return. + """ + limit: Int = 200 + ): HTML! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this repository + """ + url: URI! + + """ + Whether this repository has a custom image to use with Open Graph as opposed to being represented by the owner's avatar. + """ + usesCustomOpenGraphImage: Boolean! +} + +""" +Repository interaction limit that applies to this object. +""" +type RepositoryInteractionAbility { + """ + The time the currently active limit expires. + """ + expiresAt: DateTime + + """ + The current limit that is enabled on this object. + """ + limit: RepositoryInteractionLimit! + + """ + The origin of the currently active interaction limit. + """ + origin: RepositoryInteractionLimitOrigin! +} + +""" +A repository interaction limit. +""" +enum RepositoryInteractionLimit { + """ + Users that are not collaborators will not be able to interact with the repository. + """ + COLLABORATORS_ONLY + + """ + Users that have not previously committed to a repository’s default branch will be unable to interact with the repository. + """ + CONTRIBUTORS_ONLY + + """ + Users that have recently created their account will be unable to interact with the repository. + """ + EXISTING_USERS + + """ + No interaction limits are enabled. + """ + NO_LIMIT +} + +""" +The length for a repository interaction limit to be enabled for. +""" +enum RepositoryInteractionLimitExpiry { + """ + The interaction limit will expire after 1 day. + """ + ONE_DAY + + """ + The interaction limit will expire after 1 month. + """ + ONE_MONTH + + """ + The interaction limit will expire after 1 week. + """ + ONE_WEEK + + """ + The interaction limit will expire after 6 months. + """ + SIX_MONTHS + + """ + The interaction limit will expire after 3 days. + """ + THREE_DAYS +} + +""" +Indicates where an interaction limit is configured. +""" +enum RepositoryInteractionLimitOrigin { + """ + A limit that is configured at the organization level. + """ + ORGANIZATION + + """ + A limit that is configured at the repository level. + """ + REPOSITORY + + """ + A limit that is configured at the user-wide level. + """ + USER +} + +""" +An invitation for a user to be added to a repository. +""" +type RepositoryInvitation implements Node { + """ + The email address that received the invitation. + """ + email: String + id: ID! + + """ + The user who received the invitation. + """ + invitee: User + + """ + The user who created the invitation. + """ + inviter: User! + + """ + The permalink for this repository invitation. + """ + permalink: URI! + + """ + The permission granted on this repository by this invitation. + """ + permission: RepositoryPermission! + + """ + The Repository the user is invited to. + """ + repository: RepositoryInfo +} + +""" +The connection type for RepositoryInvitation. +""" +type RepositoryInvitationConnection { + """ + A list of edges. + """ + edges: [RepositoryInvitationEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryInvitation] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryInvitationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryInvitation +} + +""" +Ordering options for repository invitation connections. +""" +input RepositoryInvitationOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repository invitations by. + """ + field: RepositoryInvitationOrderField! +} + +""" +Properties by which repository invitation connections can be ordered. +""" +enum RepositoryInvitationOrderField { + """ + Order repository invitations by creation time + """ + CREATED_AT + + """ + Order repository invitations by invitee login + """ + INVITEE_LOGIN @deprecated(reason: "`INVITEE_LOGIN` is no longer a valid field value. Repository invitations can now be associated with an email, not only an invitee. Removal on 2020-10-01 UTC.") +} + +""" +The possible reasons a given repository could be in a locked state. +""" +enum RepositoryLockReason { + """ + The repository is locked due to a billing related reason. + """ + BILLING + + """ + The repository is locked due to a migration. + """ + MIGRATING + + """ + The repository is locked due to a move. + """ + MOVING + + """ + The repository is locked due to a rename. + """ + RENAME +} + +""" +Represents a object that belongs to a repository. +""" +interface RepositoryNode { + """ + The repository associated with this node. + """ + repository: Repository! +} + +""" +Ordering options for repository connections +""" +input RepositoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: RepositoryOrderField! +} + +""" +Properties by which repository connections can be ordered. +""" +enum RepositoryOrderField { + """ + Order repositories by creation time + """ + CREATED_AT + + """ + Order repositories by name + """ + NAME + + """ + Order repositories by push time + """ + PUSHED_AT + + """ + Order repositories by number of stargazers + """ + STARGAZERS + + """ + Order repositories by update time + """ + UPDATED_AT +} + +""" +Represents an owner of a Repository. +""" +interface RepositoryOwner { + """ + A URL pointing to the owner's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + id: ID! + + """ + The username used to login. + """ + login: String! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + The HTTP URL for the owner. + """ + resourcePath: URI! + + """ + The HTTP URL for the owner. + """ + url: URI! +} + +""" +The access level to a repository +""" +enum RepositoryPermission { + """ + Can read, clone, and push to this repository. Can also manage issues, pull + requests, and repository settings, including adding collaborators + """ + ADMIN + + """ + Can read, clone, and push to this repository. They can also manage issues, pull requests, and some repository settings + """ + MAINTAIN + + """ + Can read and clone this repository. Can also open and comment on issues and pull requests + """ + READ + + """ + Can read and clone this repository. Can also manage issues and pull requests + """ + TRIAGE + + """ + Can read, clone, and push to this repository. Can also manage issues and pull requests + """ + WRITE +} + +""" +The privacy of a repository +""" +enum RepositoryPrivacy { + """ + Private + """ + PRIVATE + + """ + Public + """ + PUBLIC +} + +""" +A repository-topic connects a repository to a topic. +""" +type RepositoryTopic implements Node & UniformResourceLocatable { + id: ID! + + """ + The HTTP path for this repository-topic. + """ + resourcePath: URI! + + """ + The topic. + """ + topic: Topic! + + """ + The HTTP URL for this repository-topic. + """ + url: URI! +} + +""" +The connection type for RepositoryTopic. +""" +type RepositoryTopicConnection { + """ + A list of edges. + """ + edges: [RepositoryTopicEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryTopic] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryTopicEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryTopic +} + +""" +The repository's visibility level. +""" +enum RepositoryVisibility { + """ + The repository is visible only to users in the same business. + """ + INTERNAL + + """ + The repository is visible only to those with explicit access. + """ + PRIVATE + + """ + The repository is visible to everyone. + """ + PUBLIC +} + +""" +Audit log entry for a repository_visibility_change.disable event. +""" +type RepositoryVisibilityChangeDisableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a repository_visibility_change.enable event. +""" +type RepositoryVisibilityChangeEnableAuditEntry implements AuditEntry & EnterpriseAuditEntryData & Node & OrganizationAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + + """ + The HTTP path for this enterprise. + """ + enterpriseResourcePath: URI + + """ + The slug of the enterprise. + """ + enterpriseSlug: String + + """ + The HTTP URL for this enterprise. + """ + enterpriseUrl: URI + id: ID! + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +A alert for a repository with an affected vulnerability. +""" +type RepositoryVulnerabilityAlert implements Node & RepositoryNode { + """ + When was the alert created? + """ + createdAt: DateTime! + + """ + The reason the alert was dismissed + """ + dismissReason: String + + """ + When was the alert dismissed? + """ + dismissedAt: DateTime + + """ + The user who dismissed the alert + """ + dismisser: User + id: ID! + + """ + The associated repository + """ + repository: Repository! + + """ + The associated security advisory + """ + securityAdvisory: SecurityAdvisory + + """ + The associated security vulnerability + """ + securityVulnerability: SecurityVulnerability + + """ + The vulnerable manifest filename + """ + vulnerableManifestFilename: String! + + """ + The vulnerable manifest path + """ + vulnerableManifestPath: String! + + """ + The vulnerable requirements + """ + vulnerableRequirements: String +} + +""" +The connection type for RepositoryVulnerabilityAlert. +""" +type RepositoryVulnerabilityAlertConnection { + """ + A list of edges. + """ + edges: [RepositoryVulnerabilityAlertEdge] + + """ + A list of nodes. + """ + nodes: [RepositoryVulnerabilityAlert] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type RepositoryVulnerabilityAlertEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RepositoryVulnerabilityAlert +} + +""" +Autogenerated input type of RequestReviews +""" +input RequestReviewsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request to modify. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Node IDs of the team to request. + """ + teamIds: [ID!] @possibleTypes(concreteTypes: ["Team"]) + + """ + Add users to the set rather than replace. + """ + union: Boolean + + """ + The Node IDs of the user to request. + """ + userIds: [ID!] @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of RequestReviews +""" +type RequestReviewsPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The pull request that is getting requests. + """ + pullRequest: PullRequest + + """ + The edge from the pull request to the requested reviewers. + """ + requestedReviewersEdge: UserEdge +} + +""" +The possible states that can be requested when creating a check run. +""" +enum RequestableCheckStatusState { + """ + The check suite or run has been completed. + """ + COMPLETED + + """ + The check suite or run is in progress. + """ + IN_PROGRESS + + """ + The check suite or run has been queued. + """ + QUEUED + + """ + The check suite or run is in waiting state. + """ + WAITING +} + +""" +Types that can be requested reviewers. +""" +union RequestedReviewer = Mannequin | Team | User + +""" +Autogenerated input type of RerequestCheckSuite +""" +input RerequestCheckSuiteInput { + """ + The Node ID of the check suite. + """ + checkSuiteId: ID! @possibleTypes(concreteTypes: ["CheckSuite"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of RerequestCheckSuite +""" +type RerequestCheckSuitePayload { + """ + The requested check suite. + """ + checkSuite: CheckSuite + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of ResolveReviewThread +""" +input ResolveReviewThreadInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the thread to resolve + """ + threadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of ResolveReviewThread +""" +type ResolveReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The thread to resolve. + """ + thread: PullRequestReviewThread +} + +""" +Represents a private contribution a user made on GitHub. +""" +type RestrictedContribution implements Contribution { + """ + Whether this contribution is associated with a record you do not have access to. For + example, your own 'first issue' contribution may have been made on a repository you can no + longer access. + """ + isRestricted: Boolean! + + """ + When this contribution was made. + """ + occurredAt: DateTime! + + """ + The HTTP path for this contribution. + """ + resourcePath: URI! + + """ + The HTTP URL for this contribution. + """ + url: URI! + + """ + The user who made this contribution. + """ + user: User! +} + +""" +A team or user who has the ability to dismiss a review on a protected branch. +""" +type ReviewDismissalAllowance implements Node { + """ + The actor that can dismiss. + """ + actor: ReviewDismissalAllowanceActor + + """ + Identifies the branch protection rule associated with the allowed user or team. + """ + branchProtectionRule: BranchProtectionRule + id: ID! +} + +""" +Types that can be an actor. +""" +union ReviewDismissalAllowanceActor = Team | User + +""" +The connection type for ReviewDismissalAllowance. +""" +type ReviewDismissalAllowanceConnection { + """ + A list of edges. + """ + edges: [ReviewDismissalAllowanceEdge] + + """ + A list of nodes. + """ + nodes: [ReviewDismissalAllowance] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewDismissalAllowanceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReviewDismissalAllowance +} + +""" +Represents a 'review_dismissed' event on a given issue or pull request. +""" +type ReviewDismissedEvent implements Node & UniformResourceLocatable { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + Identifies the optional message associated with the 'review_dismissed' event. + """ + dismissalMessage: String + + """ + Identifies the optional message associated with the event, rendered to HTML. + """ + dismissalMessageHTML: String + id: ID! + + """ + Identifies the previous state of the review with the 'review_dismissed' event. + """ + previousReviewState: PullRequestReviewState! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the commit which caused the review to become stale. + """ + pullRequestCommit: PullRequestCommit + + """ + The HTTP path for this review dismissed event. + """ + resourcePath: URI! + + """ + Identifies the review associated with the 'review_dismissed' event. + """ + review: PullRequestReview + + """ + The HTTP URL for this review dismissed event. + """ + url: URI! +} + +""" +A request for a user to review a pull request. +""" +type ReviewRequest implements Node { + """ + Whether this request was created for a code owner + """ + asCodeOwner: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + Identifies the pull request associated with this review request. + """ + pullRequest: PullRequest! + + """ + The reviewer that is requested. + """ + requestedReviewer: RequestedReviewer +} + +""" +The connection type for ReviewRequest. +""" +type ReviewRequestConnection { + """ + A list of edges. + """ + edges: [ReviewRequestEdge] + + """ + A list of nodes. + """ + nodes: [ReviewRequest] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewRequestEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ReviewRequest +} + +""" +Represents an 'review_request_removed' event on a given pull request. +""" +type ReviewRequestRemovedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the reviewer whose review request was removed. + """ + requestedReviewer: RequestedReviewer +} + +""" +Represents an 'review_requested' event on a given pull request. +""" +type ReviewRequestedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + PullRequest referenced by event. + """ + pullRequest: PullRequest! + + """ + Identifies the reviewer whose review was requested. + """ + requestedReviewer: RequestedReviewer +} + +""" +A hovercard context with a message describing the current code review state of the pull +request. +""" +type ReviewStatusHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + The current status of the pull request with respect to code review. + """ + reviewDecision: PullRequestReviewDecision +} + +""" +The possible digest algorithms used to sign SAML requests for an identity provider. +""" +enum SamlDigestAlgorithm { + """ + SHA1 + """ + SHA1 + + """ + SHA256 + """ + SHA256 + + """ + SHA384 + """ + SHA384 + + """ + SHA512 + """ + SHA512 +} + +""" +The possible signature algorithms used to sign SAML requests for a Identity Provider. +""" +enum SamlSignatureAlgorithm { + """ + RSA-SHA1 + """ + RSA_SHA1 + + """ + RSA-SHA256 + """ + RSA_SHA256 + + """ + RSA-SHA384 + """ + RSA_SHA384 + + """ + RSA-SHA512 + """ + RSA_SHA512 +} + +""" +A Saved Reply is text a user can use to reply quickly. +""" +type SavedReply implements Node { + """ + The body of the saved reply. + """ + body: String! + + """ + The saved reply body rendered to HTML. + """ + bodyHTML: HTML! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + id: ID! + + """ + The title of the saved reply. + """ + title: String! + + """ + The user that saved this reply. + """ + user: Actor +} + +""" +The connection type for SavedReply. +""" +type SavedReplyConnection { + """ + A list of edges. + """ + edges: [SavedReplyEdge] + + """ + A list of nodes. + """ + nodes: [SavedReply] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SavedReplyEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SavedReply +} + +""" +Ordering options for saved reply connections. +""" +input SavedReplyOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order saved replies by. + """ + field: SavedReplyOrderField! +} + +""" +Properties by which saved reply connections can be ordered. +""" +enum SavedReplyOrderField { + """ + Order saved reply by when they were updated. + """ + UPDATED_AT +} + +""" +The results of a search. +""" +union SearchResultItem = App | Issue | MarketplaceListing | Organization | PullRequest | Repository | User + +""" +A list of results that matched against a search query. +""" +type SearchResultItemConnection { + """ + The number of pieces of code that matched the search query. + """ + codeCount: Int! + + """ + A list of edges. + """ + edges: [SearchResultItemEdge] + + """ + The number of issues that matched the search query. + """ + issueCount: Int! + + """ + A list of nodes. + """ + nodes: [SearchResultItem] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The number of repositories that matched the search query. + """ + repositoryCount: Int! + + """ + The number of users that matched the search query. + """ + userCount: Int! + + """ + The number of wiki pages that matched the search query. + """ + wikiCount: Int! +} + +""" +An edge in a connection. +""" +type SearchResultItemEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SearchResultItem + + """ + Text matches on the result found. + """ + textMatches: [TextMatch] +} + +""" +Represents the individual results of a search. +""" +enum SearchType { + """ + Returns results matching issues in repositories. + """ + ISSUE + + """ + Returns results matching repositories. + """ + REPOSITORY + + """ + Returns results matching users and organizations on GitHub. + """ + USER +} + +""" +A GitHub Security Advisory +""" +type SecurityAdvisory implements Node { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + This is a long plaintext description of the advisory + """ + description: String! + + """ + The GitHub Security Advisory ID + """ + ghsaId: String! + id: ID! + + """ + A list of identifiers for this advisory + """ + identifiers: [SecurityAdvisoryIdentifier!]! + + """ + The organization that originated the advisory + """ + origin: String! + + """ + The permalink for the advisory + """ + permalink: URI + + """ + When the advisory was published + """ + publishedAt: DateTime! + + """ + A list of references for this advisory + """ + references: [SecurityAdvisoryReference!]! + + """ + The severity of the advisory + """ + severity: SecurityAdvisorySeverity! + + """ + A short plaintext summary of the advisory + """ + summary: String! + + """ + When the advisory was last updated + """ + updatedAt: DateTime! + + """ + Vulnerabilities associated with this Advisory + """ + vulnerabilities( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + An ecosystem to filter vulnerabilities by. + """ + ecosystem: SecurityAdvisoryEcosystem + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for the returned topics. + """ + orderBy: SecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC} + + """ + A package name to filter vulnerabilities by. + """ + package: String + + """ + A list of severities to filter vulnerabilities by. + """ + severities: [SecurityAdvisorySeverity!] + ): SecurityVulnerabilityConnection! + + """ + When the advisory was withdrawn, if it has been withdrawn + """ + withdrawnAt: DateTime +} + +""" +The connection type for SecurityAdvisory. +""" +type SecurityAdvisoryConnection { + """ + A list of edges. + """ + edges: [SecurityAdvisoryEdge] + + """ + A list of nodes. + """ + nodes: [SecurityAdvisory] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +The possible ecosystems of a security vulnerability's package. +""" +enum SecurityAdvisoryEcosystem { + """ + PHP packages hosted at packagist.org + """ + COMPOSER + + """ + Java artifacts hosted at the Maven central repository + """ + MAVEN + + """ + JavaScript packages hosted at npmjs.com + """ + NPM + + """ + .NET packages hosted at the NuGet Gallery + """ + NUGET + + """ + Python packages hosted at PyPI.org + """ + PIP + + """ + Ruby gems hosted at RubyGems.org + """ + RUBYGEMS +} + +""" +An edge in a connection. +""" +type SecurityAdvisoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SecurityAdvisory +} + +""" +A GitHub Security Advisory Identifier +""" +type SecurityAdvisoryIdentifier { + """ + The identifier type, e.g. GHSA, CVE + """ + type: String! + + """ + The identifier + """ + value: String! +} + +""" +An advisory identifier to filter results on. +""" +input SecurityAdvisoryIdentifierFilter { + """ + The identifier type. + """ + type: SecurityAdvisoryIdentifierType! + + """ + The identifier string. Supports exact or partial matching. + """ + value: String! +} + +""" +Identifier formats available for advisories. +""" +enum SecurityAdvisoryIdentifierType { + """ + Common Vulnerabilities and Exposures Identifier. + """ + CVE + + """ + GitHub Security Advisory ID. + """ + GHSA +} + +""" +Ordering options for security advisory connections +""" +input SecurityAdvisoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order security advisories by. + """ + field: SecurityAdvisoryOrderField! +} + +""" +Properties by which security advisory connections can be ordered. +""" +enum SecurityAdvisoryOrderField { + """ + Order advisories by publication time + """ + PUBLISHED_AT + + """ + Order advisories by update time + """ + UPDATED_AT +} + +""" +An individual package +""" +type SecurityAdvisoryPackage { + """ + The ecosystem the package belongs to, e.g. RUBYGEMS, NPM + """ + ecosystem: SecurityAdvisoryEcosystem! + + """ + The package name + """ + name: String! +} + +""" +An individual package version +""" +type SecurityAdvisoryPackageVersion { + """ + The package name or version + """ + identifier: String! +} + +""" +A GitHub Security Advisory Reference +""" +type SecurityAdvisoryReference { + """ + A publicly accessible reference + """ + url: URI! +} + +""" +Severity of the vulnerability. +""" +enum SecurityAdvisorySeverity { + """ + Critical. + """ + CRITICAL + + """ + High. + """ + HIGH + + """ + Low. + """ + LOW + + """ + Moderate. + """ + MODERATE +} + +""" +An individual vulnerability within an Advisory +""" +type SecurityVulnerability { + """ + The Advisory associated with this Vulnerability + """ + advisory: SecurityAdvisory! + + """ + The first version containing a fix for the vulnerability + """ + firstPatchedVersion: SecurityAdvisoryPackageVersion + + """ + A description of the vulnerable package + """ + package: SecurityAdvisoryPackage! + + """ + The severity of the vulnerability within this package + """ + severity: SecurityAdvisorySeverity! + + """ + When the vulnerability was last updated + """ + updatedAt: DateTime! + + """ + A string that describes the vulnerable package versions. + This string follows a basic syntax with a few forms. + + `= 0.2.0` denotes a single vulnerable version. + + `<= 1.0.8` denotes a version range up to and including the specified version + + `< 0.1.11` denotes a version range up to, but excluding, the specified version + + `>= 4.3.0, < 4.3.5` denotes a version range with a known minimum and maximum version. + + `>= 0.0.1` denotes a version range with a known minimum, but no known maximum + """ + vulnerableVersionRange: String! +} + +""" +The connection type for SecurityVulnerability. +""" +type SecurityVulnerabilityConnection { + """ + A list of edges. + """ + edges: [SecurityVulnerabilityEdge] + + """ + A list of nodes. + """ + nodes: [SecurityVulnerability] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SecurityVulnerabilityEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SecurityVulnerability +} + +""" +Ordering options for security vulnerability connections +""" +input SecurityVulnerabilityOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order security vulnerabilities by. + """ + field: SecurityVulnerabilityOrderField! +} + +""" +Properties by which security vulnerability connections can be ordered. +""" +enum SecurityVulnerabilityOrderField { + """ + Order vulnerability by update time + """ + UPDATED_AT +} + +""" +Autogenerated input type of SetEnterpriseIdentityProvider +""" +input SetEnterpriseIdentityProviderInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The digest algorithm used to sign SAML requests for the identity provider. + """ + digestMethod: SamlDigestAlgorithm! + + """ + The ID of the enterprise on which to set an identity provider. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The x509 certificate used by the identity provider to sign assertions and responses. + """ + idpCertificate: String! + + """ + The Issuer Entity ID for the SAML identity provider + """ + issuer: String + + """ + The signature algorithm used to sign SAML requests for the identity provider. + """ + signatureMethod: SamlSignatureAlgorithm! + + """ + The URL endpoint for the identity provider's SAML SSO. + """ + ssoUrl: URI! +} + +""" +Autogenerated return type of SetEnterpriseIdentityProvider +""" +type SetEnterpriseIdentityProviderPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The identity provider for the enterprise. + """ + identityProvider: EnterpriseIdentityProvider +} + +""" +Autogenerated input type of SetOrganizationInteractionLimit +""" +input SetOrganizationInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the organization to set a limit for. + """ + organizationId: ID! @possibleTypes(concreteTypes: ["Organization"]) +} + +""" +Autogenerated return type of SetOrganizationInteractionLimit +""" +type SetOrganizationInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The organization that the interaction limit was set for. + """ + organization: Organization +} + +""" +Autogenerated input type of SetRepositoryInteractionLimit +""" +input SetRepositoryInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the repository to set a limit for. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of SetRepositoryInteractionLimit +""" +type SetRepositoryInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that the interaction limit was set for. + """ + repository: Repository +} + +""" +Autogenerated input type of SetUserInteractionLimit +""" +input SetUserInteractionLimitInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + When this limit should expire. + """ + expiry: RepositoryInteractionLimitExpiry + + """ + The limit to set. + """ + limit: RepositoryInteractionLimit! + + """ + The ID of the user to set a limit for. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of SetUserInteractionLimit +""" +type SetUserInteractionLimitPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that the interaction limit was set for. + """ + user: User +} + +""" +Represents an S/MIME signature on a Commit or Tag. +""" +type SmimeSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Entities that can sponsor others via GitHub Sponsors +""" +union Sponsor = Organization | User + +""" +Entities that can be sponsored through GitHub Sponsors +""" +interface Sponsorable { + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + This object's sponsorships as the maintainer. + """ + sponsorshipsAsMaintainer( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + This object's sponsorships as the sponsor. + """ + sponsorshipsAsSponsor( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! +} + +""" +A GitHub Sponsors listing. +""" +type SponsorsListing implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The full description of the listing. + """ + fullDescription: String! + + """ + The full description of the listing rendered to HTML. + """ + fullDescriptionHTML: HTML! + id: ID! + + """ + The listing's full name. + """ + name: String! + + """ + The short description of the listing. + """ + shortDescription: String! + + """ + The short name of the listing. + """ + slug: String! + + """ + The published tiers for this GitHub Sponsors listing. + """ + tiers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for Sponsors tiers returned from the connection. + """ + orderBy: SponsorsTierOrder = {field: MONTHLY_PRICE_IN_CENTS, direction: ASC} + ): SponsorsTierConnection +} + +""" +A GitHub Sponsors tier associated with a GitHub Sponsors listing. +""" +type SponsorsTier implements Node { + """ + SponsorsTier information only visible to users that can administer the associated Sponsors listing. + """ + adminInfo: SponsorsTierAdminInfo + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The description of the tier. + """ + description: String! + + """ + The tier description rendered to HTML + """ + descriptionHTML: HTML! + id: ID! + + """ + How much this tier costs per month in cents. + """ + monthlyPriceInCents: Int! + + """ + How much this tier costs per month in dollars. + """ + monthlyPriceInDollars: Int! + + """ + The name of the tier. + """ + name: String! + + """ + The sponsors listing that this tier belongs to. + """ + sponsorsListing: SponsorsListing! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +SponsorsTier information only visible to users that can administer the associated Sponsors listing. +""" +type SponsorsTierAdminInfo { + """ + The sponsorships associated with this tier. + """ + sponsorships( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! +} + +""" +The connection type for SponsorsTier. +""" +type SponsorsTierConnection { + """ + A list of edges. + """ + edges: [SponsorsTierEdge] + + """ + A list of nodes. + """ + nodes: [SponsorsTier] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorsTierEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SponsorsTier +} + +""" +Ordering options for Sponsors tiers connections. +""" +input SponsorsTierOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order tiers by. + """ + field: SponsorsTierOrderField! +} + +""" +Properties by which Sponsors tiers connections can be ordered. +""" +enum SponsorsTierOrderField { + """ + Order tiers by creation time. + """ + CREATED_AT + + """ + Order tiers by their monthly price in cents + """ + MONTHLY_PRICE_IN_CENTS +} + +""" +A sponsorship relationship between a sponsor and a maintainer +""" +type Sponsorship implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The entity that is being sponsored + """ + maintainer: User! @deprecated(reason: "`Sponsorship.maintainer` will be removed. Use `Sponsorship.sponsorable` instead. Removal on 2020-04-01 UTC.") + + """ + The privacy level for this sponsorship. + """ + privacyLevel: SponsorshipPrivacy! + + """ + The user that is sponsoring. Returns null if the sponsorship is private or if sponsor is not a user. + """ + sponsor: User @deprecated(reason: "`Sponsorship.sponsor` will be removed. Use `Sponsorship.sponsorEntity` instead. Removal on 2020-10-01 UTC.") + + """ + The user or organization that is sponsoring, if you have permission to view them. + """ + sponsorEntity: Sponsor + + """ + The entity that is being sponsored + """ + sponsorable: Sponsorable! + + """ + The associated sponsorship tier + """ + tier: SponsorsTier +} + +""" +The connection type for Sponsorship. +""" +type SponsorshipConnection { + """ + A list of edges. + """ + edges: [SponsorshipEdge] + + """ + A list of nodes. + """ + nodes: [Sponsorship] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SponsorshipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Sponsorship +} + +""" +Ordering options for sponsorship connections. +""" +input SponsorshipOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order sponsorship by. + """ + field: SponsorshipOrderField! +} + +""" +Properties by which sponsorship connections can be ordered. +""" +enum SponsorshipOrderField { + """ + Order sponsorship by creation time. + """ + CREATED_AT +} + +""" +The privacy of a sponsorship +""" +enum SponsorshipPrivacy { + """ + Private + """ + PRIVATE + + """ + Public + """ + PUBLIC +} + +""" +Ways in which star connections can be ordered. +""" +input StarOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field in which to order nodes by. + """ + field: StarOrderField! +} + +""" +Properties by which star connections can be ordered. +""" +enum StarOrderField { + """ + Allows ordering a list of stars by when they were created. + """ + STARRED_AT +} + +""" +The connection type for User. +""" +type StargazerConnection { + """ + A list of edges. + """ + edges: [StargazerEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user that's starred a repository. +""" +type StargazerEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: User! + + """ + Identifies when the item was starred. + """ + starredAt: DateTime! +} + +""" +Things that can be starred. +""" +interface Starrable { + id: ID! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +The connection type for Repository. +""" +type StarredRepositoryConnection { + """ + A list of edges. + """ + edges: [StarredRepositoryEdge] + + """ + Is the list of stars for this user truncated? This is true for users that have many stars. + """ + isOverLimit: Boolean! + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a starred repository. +""" +type StarredRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: Repository! + + """ + Identifies when the item was starred. + """ + starredAt: DateTime! +} + +""" +Represents a commit status. +""" +type Status implements Node { + """ + A list of status contexts and check runs for this commit. + """ + combinedContexts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): StatusCheckRollupContextConnection! + + """ + The commit this status is attached to. + """ + commit: Commit + + """ + Looks up an individual status context by context name. + """ + context( + """ + The context name. + """ + name: String! + ): StatusContext + + """ + The individual status contexts for this commit. + """ + contexts: [StatusContext!]! + id: ID! + + """ + The combined commit status. + """ + state: StatusState! +} + +""" +Represents the rollup for both the check runs and status for a commit. +""" +type StatusCheckRollup implements Node { + """ + The commit the status and check runs are attached to. + """ + commit: Commit + + """ + A list of status contexts and check runs for this commit. + """ + contexts( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): StatusCheckRollupContextConnection! + id: ID! + + """ + The combined status for the commit. + """ + state: StatusState! +} + +""" +Types that can be inside a StatusCheckRollup context. +""" +union StatusCheckRollupContext = CheckRun | StatusContext + +""" +The connection type for StatusCheckRollupContext. +""" +type StatusCheckRollupContextConnection { + """ + A list of edges. + """ + edges: [StatusCheckRollupContextEdge] + + """ + A list of nodes. + """ + nodes: [StatusCheckRollupContext] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type StatusCheckRollupContextEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: StatusCheckRollupContext +} + +""" +Represents an individual commit status context +""" +type StatusContext implements Node { + """ + The avatar of the OAuth application or the user that created the status + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int = 40 + ): URI + + """ + This commit this status context is attached to. + """ + commit: Commit + + """ + The name of this status context. + """ + context: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The actor who created this status context. + """ + creator: Actor + + """ + The description for this status context. + """ + description: String + id: ID! + + """ + The state of this status context. + """ + state: StatusState! + + """ + The URL for this status context. + """ + targetUrl: URI +} + +""" +The possible commit status states. +""" +enum StatusState { + """ + Status is errored. + """ + ERROR + + """ + Status is expected. + """ + EXPECTED + + """ + Status is failing. + """ + FAILURE + + """ + Status is pending. + """ + PENDING + + """ + Status is successful. + """ + SUCCESS +} + +""" +Autogenerated input type of SubmitPullRequestReview +""" +input SubmitPullRequestReviewInput { + """ + The text field to set on the Pull Request Review. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The event to send to the Pull Request Review. + """ + event: PullRequestReviewEvent! + + """ + The Pull Request ID to submit any pending reviews. + """ + pullRequestId: ID @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The Pull Request Review ID to submit. + """ + pullRequestReviewId: ID @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of SubmitPullRequestReview +""" +type SubmitPullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The submitted pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +A pointer to a repository at a specific revision embedded inside another repository. +""" +type Submodule { + """ + The branch of the upstream submodule for tracking updates + """ + branch: String + + """ + The git URL of the submodule repository + """ + gitUrl: URI! + + """ + The name of the submodule in .gitmodules + """ + name: String! + + """ + The path in the superproject that this submodule is located in + """ + path: String! + + """ + The commit revision of the subproject repository being tracked by the submodule + """ + subprojectCommitOid: GitObjectID +} + +""" +The connection type for Submodule. +""" +type SubmoduleConnection { + """ + A list of edges. + """ + edges: [SubmoduleEdge] + + """ + A list of nodes. + """ + nodes: [Submodule] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SubmoduleEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Submodule +} + +""" +Entities that can be subscribed to for web and email notifications. +""" +interface Subscribable { + id: ID! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Represents a 'subscribed' event on a given `Subscribable`. +""" +type SubscribedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Object referenced by event. + """ + subscribable: Subscribable! +} + +""" +The possible states of a subscription. +""" +enum SubscriptionState { + """ + The User is never notified. + """ + IGNORED + + """ + The User is notified of all conversations. + """ + SUBSCRIBED + + """ + The User is only notified when participating or @mentioned. + """ + UNSUBSCRIBED +} + +""" +A suggestion to review a pull request based on a user's commit history and review comments. +""" +type SuggestedReviewer { + """ + Is this suggestion based on past commits? + """ + isAuthor: Boolean! + + """ + Is this suggestion based on past review comments? + """ + isCommenter: Boolean! + + """ + Identifies the user suggested to review the pull request. + """ + reviewer: User! +} + +""" +Represents a Git tag. +""" +type Tag implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + id: ID! + + """ + The Git tag message. + """ + message: String + + """ + The Git tag name. + """ + name: String! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! + + """ + Details about the tag author. + """ + tagger: GitActor + + """ + The Git object the tag points to. + """ + target: GitObject! +} + +""" +A team of users in an organization. +""" +type Team implements MemberStatusable & Node & Subscribable { + """ + A list of teams that are ancestors of this team. + """ + ancestors( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TeamConnection! + + """ + A URL pointing to the team's avatar. + """ + avatarUrl( + """ + The size in pixels of the resulting square image. + """ + size: Int = 400 + ): URI + + """ + List of child teams belonging to this team + """ + childTeams( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether to list immediate child teams or all descendant child teams. + """ + immediateOnly: Boolean = true + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamOrder + + """ + User logins to filter by + """ + userLogins: [String!] + ): TeamConnection! + + """ + The slug corresponding to the organization and team. + """ + combinedSlug: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The description of the team. + """ + description: String + + """ + Find a team discussion by its number. + """ + discussion( + """ + The sequence number of the discussion to find. + """ + number: Int! + ): TeamDiscussion + + """ + A list of team discussions. + """ + discussions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If provided, filters discussions according to whether or not they are pinned. + """ + isPinned: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamDiscussionOrder + ): TeamDiscussionConnection! + + """ + The HTTP path for team discussions + """ + discussionsResourcePath: URI! + + """ + The HTTP URL for team discussions + """ + discussionsUrl: URI! + + """ + The HTTP path for editing this team + """ + editTeamResourcePath: URI! + + """ + The HTTP URL for editing this team + """ + editTeamUrl: URI! + id: ID! + + """ + A list of pending invitations for users to this team + """ + invitations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationInvitationConnection + + """ + Get the status messages members of this entity have set that are either public or visible only to the organization. + """ + memberStatuses( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for user statuses returned from the connection. + """ + orderBy: UserStatusOrder = {field: UPDATED_AT, direction: DESC} + ): UserStatusConnection! + + """ + A list of users who are members of this team. + """ + members( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter by membership type + """ + membership: TeamMembershipType = ALL + + """ + Order for the connection. + """ + orderBy: TeamMemberOrder + + """ + The search string to look for. + """ + query: String + + """ + Filter by team member role + """ + role: TeamMemberRole + ): TeamMemberConnection! + + """ + The HTTP path for the team' members + """ + membersResourcePath: URI! + + """ + The HTTP URL for the team' members + """ + membersUrl: URI! + + """ + The name of the team. + """ + name: String! + + """ + The HTTP path creating a new team + """ + newTeamResourcePath: URI! + + """ + The HTTP URL creating a new team + """ + newTeamUrl: URI! + + """ + The organization that owns this team. + """ + organization: Organization! + + """ + The parent team of the team. + """ + parentTeam: Team + + """ + The level of privacy the team has. + """ + privacy: TeamPrivacy! + + """ + A list of repositories this team has access to. + """ + repositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for the connection. + """ + orderBy: TeamRepositoryOrder + + """ + The search string to look for. + """ + query: String + ): TeamRepositoryConnection! + + """ + The HTTP path for this team's repositories + """ + repositoriesResourcePath: URI! + + """ + The HTTP URL for this team's repositories + """ + repositoriesUrl: URI! + + """ + The HTTP path for this team + """ + resourcePath: URI! + + """ + What algorithm is used for review assignment for this team + """ + reviewRequestDelegationAlgorithm: TeamReviewAssignmentAlgorithm @preview(toggledBy: "stone-crop-preview") + + """ + True if review assignment is enabled for this team + """ + reviewRequestDelegationEnabled: Boolean! @preview(toggledBy: "stone-crop-preview") + + """ + How many team members are required for review assignment for this team + """ + reviewRequestDelegationMemberCount: Int @preview(toggledBy: "stone-crop-preview") + + """ + When assigning team members via delegation, whether the entire team should be notified as well. + """ + reviewRequestDelegationNotifyTeam: Boolean! @preview(toggledBy: "stone-crop-preview") + + """ + The slug corresponding to the team. + """ + slug: String! + + """ + The HTTP path for this team's teams + """ + teamsResourcePath: URI! + + """ + The HTTP URL for this team's teams + """ + teamsUrl: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this team + """ + url: URI! + + """ + Team is adminable by the viewer. + """ + viewerCanAdminister: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +Audit log entry for a team.add_member event. +""" +type TeamAddMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a team.add_repository event. +""" +type TeamAddRepositoryAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Metadata for an audit entry with action team.* +""" +interface TeamAuditEntryData { + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI +} + +""" +Audit log entry for a team.change_parent_team event. +""" +type TeamChangeParentTeamAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The new parent team. + """ + parentTeam: Team + + """ + The name of the new parent team + """ + parentTeamName: String + + """ + The name of the former parent team + """ + parentTeamNameWas: String + + """ + The HTTP path for the parent team + """ + parentTeamResourcePath: URI + + """ + The HTTP URL for the parent team + """ + parentTeamUrl: URI + + """ + The former parent team. + """ + parentTeamWas: Team + + """ + The HTTP path for the previous parent team + """ + parentTeamWasResourcePath: URI + + """ + The HTTP URL for the previous parent team + """ + parentTeamWasUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The connection type for Team. +""" +type TeamConnection { + """ + A list of edges. + """ + edges: [TeamEdge] + + """ + A list of nodes. + """ + nodes: [Team] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +A team discussion. +""" +type TeamDiscussion implements Comment & Deletable & Node & Reactable & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the discussion's team. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + Identifies the discussion body hash. + """ + bodyVersion: String! + + """ + A list of comments on this discussion. + """ + comments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + When provided, filters the connection such that results begin with the comment with this number. + """ + fromComment: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: TeamDiscussionCommentOrder + ): TeamDiscussionCommentConnection! + + """ + The HTTP path for discussion comments + """ + commentsResourcePath: URI! + + """ + The HTTP URL for discussion comments + """ + commentsUrl: URI! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + Whether or not the discussion is pinned. + """ + isPinned: Boolean! + + """ + Whether or not the discussion is only visible to team members and org admins. + """ + isPrivate: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies the discussion within its team. + """ + number: Int! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The HTTP path for this discussion + """ + resourcePath: URI! + + """ + The team that defines the context of this discussion. + """ + team: Team! + + """ + The title of the discussion + """ + title: String! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this discussion + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Whether or not the current viewer can pin this discussion. + """ + viewerCanPin: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the viewer is able to change their subscription status for the repository. + """ + viewerCanSubscribe: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! + + """ + Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + """ + viewerSubscription: SubscriptionState +} + +""" +A comment on a team discussion. +""" +type TeamDiscussionComment implements Comment & Deletable & Node & Reactable & UniformResourceLocatable & Updatable & UpdatableComment { + """ + The actor who authored the comment. + """ + author: Actor + + """ + Author's association with the comment's team. + """ + authorAssociation: CommentAuthorAssociation! + + """ + The body as Markdown. + """ + body: String! + + """ + The body rendered to HTML. + """ + bodyHTML: HTML! + + """ + The body rendered to text. + """ + bodyText: String! + + """ + The current version of the body content. + """ + bodyVersion: String! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Check if this comment was created via an email reply. + """ + createdViaEmail: Boolean! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The discussion this comment is about. + """ + discussion: TeamDiscussion! + + """ + The actor who edited the comment. + """ + editor: Actor + id: ID! + + """ + Check if this comment was edited and includes an edit with the creation data + """ + includesCreatedEdit: Boolean! + + """ + The moment the editor made the last edit + """ + lastEditedAt: DateTime + + """ + Identifies the comment number. + """ + number: Int! + + """ + Identifies when the comment was published at. + """ + publishedAt: DateTime + + """ + A list of reactions grouped by content left on the subject. + """ + reactionGroups: [ReactionGroup!] + + """ + A list of Reactions left on the Issue. + """ + reactions( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Allows filtering Reactions by emoji. + """ + content: ReactionContent + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Allows specifying the order in which reactions are returned. + """ + orderBy: ReactionOrder + ): ReactionConnection! + + """ + The HTTP path for this comment + """ + resourcePath: URI! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this comment + """ + url: URI! + + """ + A list of edits to this content. + """ + userContentEdits( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserContentEditConnection + + """ + Check if the current viewer can delete this object. + """ + viewerCanDelete: Boolean! + + """ + Can user react to this subject + """ + viewerCanReact: Boolean! + + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! + + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + """ + Did the viewer author this comment. + """ + viewerDidAuthor: Boolean! +} + +""" +The connection type for TeamDiscussionComment. +""" +type TeamDiscussionCommentConnection { + """ + A list of edges. + """ + edges: [TeamDiscussionCommentEdge] + + """ + A list of nodes. + """ + nodes: [TeamDiscussionComment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type TeamDiscussionCommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TeamDiscussionComment +} + +""" +Ways in which team discussion comment connections can be ordered. +""" +input TeamDiscussionCommentOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: TeamDiscussionCommentOrderField! +} + +""" +Properties by which team discussion comment connections can be ordered. +""" +enum TeamDiscussionCommentOrderField { + """ + Allows sequential ordering of team discussion comments (which is equivalent to chronological ordering). + """ + NUMBER +} + +""" +The connection type for TeamDiscussion. +""" +type TeamDiscussionConnection { + """ + A list of edges. + """ + edges: [TeamDiscussionEdge] + + """ + A list of nodes. + """ + nodes: [TeamDiscussion] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type TeamDiscussionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TeamDiscussion +} + +""" +Ways in which team discussion connections can be ordered. +""" +input TeamDiscussionOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field by which to order nodes. + """ + field: TeamDiscussionOrderField! +} + +""" +Properties by which team discussion connections can be ordered. +""" +enum TeamDiscussionOrderField { + """ + Allows chronological ordering of team discussions. + """ + CREATED_AT +} + +""" +An edge in a connection. +""" +type TeamEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Team +} + +""" +The connection type for User. +""" +type TeamMemberConnection { + """ + A list of edges. + """ + edges: [TeamMemberEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a user who is a member of a team. +""" +type TeamMemberEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The HTTP path to the organization's member access page. + """ + memberAccessResourcePath: URI! + + """ + The HTTP URL to the organization's member access page. + """ + memberAccessUrl: URI! + node: User! + + """ + The role the member has on the team. + """ + role: TeamMemberRole! +} + +""" +Ordering options for team member connections +""" +input TeamMemberOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order team members by. + """ + field: TeamMemberOrderField! +} + +""" +Properties by which team member connections can be ordered. +""" +enum TeamMemberOrderField { + """ + Order team members by creation time + """ + CREATED_AT + + """ + Order team members by login + """ + LOGIN +} + +""" +The possible team member roles; either 'maintainer' or 'member'. +""" +enum TeamMemberRole { + """ + A team maintainer has permission to add and remove team members. + """ + MAINTAINER + + """ + A team member has no administrative permissions on the team. + """ + MEMBER +} + +""" +Defines which types of team members are included in the returned list. Can be one of IMMEDIATE, CHILD_TEAM or ALL. +""" +enum TeamMembershipType { + """ + Includes immediate and child team members for the team. + """ + ALL + + """ + Includes only child team members for the team. + """ + CHILD_TEAM + + """ + Includes only immediate members of the team. + """ + IMMEDIATE +} + +""" +Ways in which team connections can be ordered. +""" +input TeamOrder { + """ + The direction in which to order nodes. + """ + direction: OrderDirection! + + """ + The field in which to order nodes by. + """ + field: TeamOrderField! +} + +""" +Properties by which team connections can be ordered. +""" +enum TeamOrderField { + """ + Allows ordering a list of teams by name. + """ + NAME +} + +""" +The possible team privacy values. +""" +enum TeamPrivacy { + """ + A secret team can only be seen by its members. + """ + SECRET + + """ + A visible team can be seen and @mentioned by every member of the organization. + """ + VISIBLE +} + +""" +Audit log entry for a team.remove_member event. +""" +type TeamRemoveMemberAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +Audit log entry for a team.remove_repository event. +""" +type TeamRemoveRepositoryAuditEntry implements AuditEntry & Node & OrganizationAuditEntryData & RepositoryAuditEntryData & TeamAuditEntryData { + """ + The action name + """ + action: String! + + """ + The user who initiated the action + """ + actor: AuditEntryActor + + """ + The IP address of the actor + """ + actorIp: String + + """ + A readable representation of the actor's location + """ + actorLocation: ActorLocation + + """ + The username of the user who initiated the action + """ + actorLogin: String + + """ + The HTTP path for the actor. + """ + actorResourcePath: URI + + """ + The HTTP URL for the actor. + """ + actorUrl: URI + + """ + The time the action was initiated + """ + createdAt: PreciseDateTime! + id: ID! + + """ + Whether the team was mapped to an LDAP Group. + """ + isLdapMapped: Boolean + + """ + The corresponding operation type for the action + """ + operationType: OperationType + + """ + The Organization associated with the Audit Entry. + """ + organization: Organization + + """ + The name of the Organization. + """ + organizationName: String + + """ + The HTTP path for the organization + """ + organizationResourcePath: URI + + """ + The HTTP URL for the organization + """ + organizationUrl: URI + + """ + The repository associated with the action + """ + repository: Repository + + """ + The name of the repository + """ + repositoryName: String + + """ + The HTTP path for the repository + """ + repositoryResourcePath: URI + + """ + The HTTP URL for the repository + """ + repositoryUrl: URI + + """ + The team associated with the action + """ + team: Team + + """ + The name of the team + """ + teamName: String + + """ + The HTTP path for this team + """ + teamResourcePath: URI + + """ + The HTTP URL for this team + """ + teamUrl: URI + + """ + The user affected by the action + """ + user: User + + """ + For actions involving two users, the actor is the initiator and the user is the affected user. + """ + userLogin: String + + """ + The HTTP path for the user. + """ + userResourcePath: URI + + """ + The HTTP URL for the user. + """ + userUrl: URI +} + +""" +The connection type for Repository. +""" +type TeamRepositoryConnection { + """ + A list of edges. + """ + edges: [TeamRepositoryEdge] + + """ + A list of nodes. + """ + nodes: [Repository] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +Represents a team repository. +""" +type TeamRepositoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + node: Repository! + + """ + The permission level the team has on the repository + """ + permission: RepositoryPermission! +} + +""" +Ordering options for team repository connections +""" +input TeamRepositoryOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order repositories by. + """ + field: TeamRepositoryOrderField! +} + +""" +Properties by which team repository connections can be ordered. +""" +enum TeamRepositoryOrderField { + """ + Order repositories by creation time + """ + CREATED_AT + + """ + Order repositories by name + """ + NAME + + """ + Order repositories by permission + """ + PERMISSION + + """ + Order repositories by push time + """ + PUSHED_AT + + """ + Order repositories by number of stargazers + """ + STARGAZERS + + """ + Order repositories by update time + """ + UPDATED_AT +} + +""" +The possible team review assignment algorithms +""" +enum TeamReviewAssignmentAlgorithm @preview(toggledBy: "stone-crop-preview") { + """ + Balance review load across the entire team + """ + LOAD_BALANCE + + """ + Alternate reviews between each team member + """ + ROUND_ROBIN +} + +""" +The role of a user on a team. +""" +enum TeamRole { + """ + User has admin rights on the team. + """ + ADMIN + + """ + User is a member of the team. + """ + MEMBER +} + +""" +A text match within a search result. +""" +type TextMatch { + """ + The specific text fragment within the property matched on. + """ + fragment: String! + + """ + Highlights within the matched fragment. + """ + highlights: [TextMatchHighlight!]! + + """ + The property matched on. + """ + property: String! +} + +""" +Represents a single highlight in a search result match. +""" +type TextMatchHighlight { + """ + The indice in the fragment where the matched text begins. + """ + beginIndice: Int! + + """ + The indice in the fragment where the matched text ends. + """ + endIndice: Int! + + """ + The text matched. + """ + text: String! +} + +""" +A topic aggregates entities that are related to a subject. +""" +type Topic implements Node & Starrable { + id: ID! + + """ + The topic's name. + """ + name: String! + + """ + A list of related topics, including aliases of this topic, sorted with the most relevant + first. Returns up to 10 Topics. + """ + relatedTopics( + """ + How many topics to return. + """ + first: Int = 3 + ): [Topic!]! + + """ + Returns a count of how many stargazers there are on this object + """ + stargazerCount: Int! + + """ + A list of users who have starred this starrable. + """ + stargazers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + ): StargazerConnection! + + """ + Returns a boolean indicating whether the viewing user has starred this starrable. + """ + viewerHasStarred: Boolean! +} + +""" +Metadata for an audit entry with a topic. +""" +interface TopicAuditEntryData { + """ + The name of the topic added to the repository + """ + topic: Topic + + """ + The name of the topic added to the repository + """ + topicName: String +} + +""" +Reason that the suggested topic is declined. +""" +enum TopicSuggestionDeclineReason { + """ + The suggested topic is not relevant to the repository. + """ + NOT_RELEVANT + + """ + The viewer does not like the suggested topic. + """ + PERSONAL_PREFERENCE + + """ + The suggested topic is too general for the repository. + """ + TOO_GENERAL + + """ + The suggested topic is too specific for the repository (e.g. #ruby-on-rails-version-4-2-1). + """ + TOO_SPECIFIC +} + +""" +Autogenerated input type of TransferIssue +""" +input TransferIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the issue to be transferred + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + The Node ID of the repository the issue should be transferred to + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of TransferIssue +""" +type TransferIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was transferred + """ + issue: Issue +} + +""" +Represents a 'transferred' event on a given issue or pull request. +""" +type TransferredEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The repository this came from + """ + fromRepository: Repository + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +Represents a Git tree. +""" +type Tree implements GitObject & Node { + """ + An abbreviated version of the Git object ID + """ + abbreviatedOid: String! + + """ + The HTTP path for this Git object + """ + commitResourcePath: URI! + + """ + The HTTP URL for this Git object + """ + commitUrl: URI! + + """ + A list of tree entries. + """ + entries: [TreeEntry!] + id: ID! + + """ + The Git object ID + """ + oid: GitObjectID! + + """ + The Repository the Git object belongs to + """ + repository: Repository! +} + +""" +Represents a Git tree entry. +""" +type TreeEntry { + """ + The extension of the file + """ + extension: String + + """ + Whether or not this tree entry is generated + """ + isGenerated: Boolean! + + """ + Entry file mode. + """ + mode: Int! + + """ + Entry file name. + """ + name: String! + + """ + Entry file object. + """ + object: GitObject + + """ + Entry file Git object ID. + """ + oid: GitObjectID! + + """ + The full path of the file. + """ + path: String + + """ + The Repository the tree entry belongs to + """ + repository: Repository! + + """ + If the TreeEntry is for a directory occupied by a submodule project, this returns the corresponding submodule + """ + submodule: Submodule + + """ + Entry file type. + """ + type: String! +} + +""" +An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. +""" +scalar URI + +""" +Autogenerated input type of UnarchiveRepository +""" +input UnarchiveRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the repository to unarchive. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnarchiveRepository +""" +type UnarchiveRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The repository that was unarchived. + """ + repository: Repository +} + +""" +Represents an 'unassigned' event on any assignable object. +""" +type UnassignedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the assignable associated with the event. + """ + assignable: Assignable! + + """ + Identifies the user or mannequin that was unassigned. + """ + assignee: Assignee + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the subject (user) who was unassigned. + """ + user: User @deprecated(reason: "Assignees can now be mannequins. Use the `assignee` field instead. Removal on 2020-01-01 UTC.") +} + +""" +Autogenerated input type of UnfollowUser +""" +input UnfollowUserInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the user to unfollow. + """ + userId: ID! @possibleTypes(concreteTypes: ["User"]) +} + +""" +Autogenerated return type of UnfollowUser +""" +type UnfollowUserPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The user that was unfollowed. + """ + user: User +} + +""" +Represents a type that can be retrieved by a URL. +""" +interface UniformResourceLocatable { + """ + The HTML path to this resource. + """ + resourcePath: URI! + + """ + The URL to this resource. + """ + url: URI! +} + +""" +Represents an unknown signature on a Commit or Tag. +""" +type UnknownSignature implements GitSignature { + """ + Email used to sign this object. + """ + email: String! + + """ + True if the signature is valid and verified by GitHub. + """ + isValid: Boolean! + + """ + Payload for GPG signing object. Raw ODB object without the signature header. + """ + payload: String! + + """ + ASCII-armored signature header from object. + """ + signature: String! + + """ + GitHub user corresponding to the email signing this commit. + """ + signer: User + + """ + The state of this signature. `VALID` if signature is valid and verified by + GitHub, otherwise represents reason why signature is considered invalid. + """ + state: GitSignatureState! + + """ + True if the signature was made with GitHub's signing key. + """ + wasSignedByGitHub: Boolean! +} + +""" +Represents an 'unlabeled' event on a given issue or pull request. +""" +type UnlabeledEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the label associated with the 'unlabeled' event. + """ + label: Label! + + """ + Identifies the `Labelable` associated with the event. + """ + labelable: Labelable! +} + +""" +Autogenerated input type of UnlinkRepositoryFromProject +""" +input UnlinkRepositoryFromProjectInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Project linked to the Repository. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + The ID of the Repository linked to the Project. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UnlinkRepositoryFromProject +""" +type UnlinkRepositoryFromProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The linked Project. + """ + project: Project + + """ + The linked Repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UnlockLockable +""" +input UnlockLockableInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the item to be unlocked. + """ + lockableId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "Lockable") +} + +""" +Autogenerated return type of UnlockLockable +""" +type UnlockLockablePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The item that was unlocked. + """ + unlockedRecord: Lockable +} + +""" +Represents an 'unlocked' event on a given issue or pull request. +""" +type UnlockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Object that was unlocked. + """ + lockable: Lockable! +} + +""" +Autogenerated input type of UnmarkFileAsViewed +""" +input UnmarkFileAsViewedInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The path of the file to mark as unviewed + """ + path: String! + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) +} + +""" +Autogenerated return type of UnmarkFileAsViewed +""" +type UnmarkFileAsViewedPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UnmarkIssueAsDuplicate +""" +input UnmarkIssueAsDuplicateInput { + """ + ID of the issue or pull request currently considered canonical/authoritative/original. + """ + canonicalId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + ID of the issue or pull request currently marked as a duplicate. + """ + duplicateId: ID! @possibleTypes(concreteTypes: ["Issue", "PullRequest"], abstractType: "IssueOrPullRequest") +} + +""" +Autogenerated return type of UnmarkIssueAsDuplicate +""" +type UnmarkIssueAsDuplicatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue or pull request that was marked as a duplicate. + """ + duplicate: IssueOrPullRequest +} + +""" +Represents an 'unmarked_as_duplicate' event on a given issue or pull request. +""" +type UnmarkedAsDuplicateEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + The authoritative issue or pull request which has been duplicated by another. + """ + canonical: IssueOrPullRequest + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + The issue or pull request which has been marked as a duplicate of another. + """ + duplicate: IssueOrPullRequest + id: ID! + + """ + Canonical and duplicate belong to different repositories. + """ + isCrossRepository: Boolean! +} + +""" +Autogenerated input type of UnminimizeComment +""" +input UnminimizeCommentInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the subject to modify. + """ + subjectId: ID! @possibleTypes(concreteTypes: ["CommitComment", "GistComment", "IssueComment", "PullRequestReviewComment"], abstractType: "Minimizable") +} + +""" +Autogenerated return type of UnminimizeComment +""" +type UnminimizeCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The comment that was unminimized. + """ + unminimizedComment: Minimizable +} + +""" +Autogenerated input type of UnpinIssue +""" +input UnpinIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the issue to be unpinned + """ + issueId: ID! @possibleTypes(concreteTypes: ["Issue"]) +} + +""" +Autogenerated return type of UnpinIssue +""" +type UnpinIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue that was unpinned + """ + issue: Issue +} + +""" +Represents an 'unpinned' event on a given issue or pull request. +""" +type UnpinnedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Identifies the issue associated with the event. + """ + issue: Issue! +} + +""" +Autogenerated input type of UnresolveReviewThread +""" +input UnresolveReviewThreadInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the thread to unresolve + """ + threadId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewThread"]) +} + +""" +Autogenerated return type of UnresolveReviewThread +""" +type UnresolveReviewThreadPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The thread to resolve. + """ + thread: PullRequestReviewThread +} + +""" +Represents an 'unsubscribed' event on a given `Subscribable`. +""" +type UnsubscribedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + Object referenced by event. + """ + subscribable: Subscribable! +} + +""" +Entities that can be updated. +""" +interface Updatable { + """ + Check if the current viewer can update this object. + """ + viewerCanUpdate: Boolean! +} + +""" +Comments that can be updated. +""" +interface UpdatableComment { + """ + Reasons why the current viewer can not update this comment. + """ + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! +} + +""" +Autogenerated input type of UpdateBranchProtectionRule +""" +input UpdateBranchProtectionRuleInput { + """ + Can this branch be deleted. + """ + allowsDeletions: Boolean + + """ + Are force pushes allowed on this branch. + """ + allowsForcePushes: Boolean + + """ + The global relay id of the branch protection rule to be updated. + """ + branchProtectionRuleId: ID! @possibleTypes(concreteTypes: ["BranchProtectionRule"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Will new commits pushed to matching branches dismiss pull request review approvals. + """ + dismissesStaleReviews: Boolean + + """ + Can admins overwrite branch protection. + """ + isAdminEnforced: Boolean + + """ + The glob-like pattern used to determine matching branches. + """ + pattern: String + + """ + A list of User, Team or App IDs allowed to push to matching branches. + """ + pushActorIds: [ID!] + + """ + Number of approving reviews required to update matching branches. + """ + requiredApprovingReviewCount: Int + + """ + List of required status check contexts that must pass for commits to be accepted to matching branches. + """ + requiredStatusCheckContexts: [String!] + + """ + Are approving reviews required to update matching branches. + """ + requiresApprovingReviews: Boolean + + """ + Are reviews from code owners required to update matching branches. + """ + requiresCodeOwnerReviews: Boolean + + """ + Are commits required to be signed. + """ + requiresCommitSignatures: Boolean + + """ + Are merge commits prohibited from being pushed to this branch. + """ + requiresLinearHistory: Boolean + + """ + Are status checks required to update matching branches. + """ + requiresStatusChecks: Boolean + + """ + Are branches required to be up to date before merging. + """ + requiresStrictStatusChecks: Boolean + + """ + Is pushing to matching branches restricted. + """ + restrictsPushes: Boolean + + """ + Is dismissal of pull request reviews restricted. + """ + restrictsReviewDismissals: Boolean + + """ + A list of User or Team IDs allowed to dismiss reviews on pull requests targeting matching branches. + """ + reviewDismissalActorIds: [ID!] +} + +""" +Autogenerated return type of UpdateBranchProtectionRule +""" +type UpdateBranchProtectionRulePayload { + """ + The newly created BranchProtectionRule. + """ + branchProtectionRule: BranchProtectionRule + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateCheckRun +""" +input UpdateCheckRunInput { + """ + Possible further actions the integrator can perform, which a user may trigger. + """ + actions: [CheckRunAction!] + + """ + The node of the check. + """ + checkRunId: ID! @possibleTypes(concreteTypes: ["CheckRun"]) + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The time that the check run finished. + """ + completedAt: DateTime + + """ + The final conclusion of the check. + """ + conclusion: CheckConclusionState + + """ + The URL of the integrator's site that has the full details of the check. + """ + detailsUrl: URI + + """ + A reference for the run on the integrator's system. + """ + externalId: String + + """ + The name of the check. + """ + name: String + + """ + Descriptive details about the run. + """ + output: CheckRunOutput + + """ + The node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + The time that the check run began. + """ + startedAt: DateTime + + """ + The current status. + """ + status: RequestableCheckStatusState +} + +""" +Autogenerated return type of UpdateCheckRun +""" +type UpdateCheckRunPayload { + """ + The updated check run. + """ + checkRun: CheckRun + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateCheckSuitePreferences +""" +input UpdateCheckSuitePreferencesInput { + """ + The check suite preferences to modify. + """ + autoTriggerPreferences: [CheckSuiteAutoTriggerPreference!]! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UpdateCheckSuitePreferences +""" +type UpdateCheckSuitePreferencesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateEnterpriseAdministratorRole +""" +input UpdateEnterpriseAdministratorRoleInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Enterprise which the admin belongs to. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The login of a administrator whose role is being changed. + """ + login: String! + + """ + The new role for the Enterprise administrator. + """ + role: EnterpriseAdministratorRole! +} + +""" +Autogenerated return type of UpdateEnterpriseAdministratorRole +""" +type UpdateEnterpriseAdministratorRolePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A message confirming the result of changing the administrator's role. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseAllowPrivateRepositoryForkingSetting +""" +input UpdateEnterpriseAllowPrivateRepositoryForkingSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the allow private repository forking setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the allow private repository forking setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseAllowPrivateRepositoryForkingSetting +""" +type UpdateEnterpriseAllowPrivateRepositoryForkingSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated allow private repository forking setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the allow private repository forking setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseDefaultRepositoryPermissionSetting +""" +input UpdateEnterpriseDefaultRepositoryPermissionSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the default repository permission setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the default repository permission setting on the enterprise. + """ + settingValue: EnterpriseDefaultRepositoryPermissionSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseDefaultRepositoryPermissionSetting +""" +type UpdateEnterpriseDefaultRepositoryPermissionSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated default repository permission setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the default repository permission setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting +""" +input UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can change repository visibility setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can change repository visibility setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanChangeRepositoryVisibilitySetting +""" +type UpdateEnterpriseMembersCanChangeRepositoryVisibilitySettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can change repository visibility setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can change repository visibility setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanCreateRepositoriesSetting +""" +input UpdateEnterpriseMembersCanCreateRepositoriesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can create repositories setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + Allow members to create internal repositories. Defaults to current value. + """ + membersCanCreateInternalRepositories: Boolean + + """ + Allow members to create private repositories. Defaults to current value. + """ + membersCanCreatePrivateRepositories: Boolean + + """ + Allow members to create public repositories. Defaults to current value. + """ + membersCanCreatePublicRepositories: Boolean + + """ + When false, allow member organizations to set their own repository creation member privileges. + """ + membersCanCreateRepositoriesPolicyEnabled: Boolean + + """ + Value for the members can create repositories setting on the enterprise. This + or the granular public/private/internal allowed fields (but not both) must be provided. + """ + settingValue: EnterpriseMembersCanCreateRepositoriesSettingValue +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanCreateRepositoriesSetting +""" +type UpdateEnterpriseMembersCanCreateRepositoriesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can create repositories setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can create repositories setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanDeleteIssuesSetting +""" +input UpdateEnterpriseMembersCanDeleteIssuesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can delete issues setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can delete issues setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanDeleteIssuesSetting +""" +type UpdateEnterpriseMembersCanDeleteIssuesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can delete issues setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can delete issues setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanDeleteRepositoriesSetting +""" +input UpdateEnterpriseMembersCanDeleteRepositoriesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can delete repositories setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can delete repositories setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanDeleteRepositoriesSetting +""" +type UpdateEnterpriseMembersCanDeleteRepositoriesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can delete repositories setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can delete repositories setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanInviteCollaboratorsSetting +""" +input UpdateEnterpriseMembersCanInviteCollaboratorsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can invite collaborators setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can invite collaborators setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanInviteCollaboratorsSetting +""" +type UpdateEnterpriseMembersCanInviteCollaboratorsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can invite collaborators setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can invite collaborators setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanMakePurchasesSetting +""" +input UpdateEnterpriseMembersCanMakePurchasesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can make purchases setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can make purchases setting on the enterprise. + """ + settingValue: EnterpriseMembersCanMakePurchasesSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanMakePurchasesSetting +""" +type UpdateEnterpriseMembersCanMakePurchasesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can make purchases setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can make purchases setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting +""" +input UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can update protected branches setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can update protected branches setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanUpdateProtectedBranchesSetting +""" +type UpdateEnterpriseMembersCanUpdateProtectedBranchesSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can update protected branches setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can update protected branches setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseMembersCanViewDependencyInsightsSetting +""" +input UpdateEnterpriseMembersCanViewDependencyInsightsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the members can view dependency insights setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the members can view dependency insights setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseMembersCanViewDependencyInsightsSetting +""" +type UpdateEnterpriseMembersCanViewDependencyInsightsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated members can view dependency insights setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the members can view dependency insights setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseOrganizationProjectsSetting +""" +input UpdateEnterpriseOrganizationProjectsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the organization projects setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the organization projects setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseOrganizationProjectsSetting +""" +type UpdateEnterpriseOrganizationProjectsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated organization projects setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the organization projects setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseProfile +""" +input UpdateEnterpriseProfileInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The description of the enterprise. + """ + description: String + + """ + The Enterprise ID to update. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The location of the enterprise. + """ + location: String + + """ + The name of the enterprise. + """ + name: String + + """ + The URL of the enterprise's website. + """ + websiteUrl: String +} + +""" +Autogenerated return type of UpdateEnterpriseProfile +""" +type UpdateEnterpriseProfilePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated enterprise. + """ + enterprise: Enterprise +} + +""" +Autogenerated input type of UpdateEnterpriseRepositoryProjectsSetting +""" +input UpdateEnterpriseRepositoryProjectsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the repository projects setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the repository projects setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseRepositoryProjectsSetting +""" +type UpdateEnterpriseRepositoryProjectsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated repository projects setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the repository projects setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTeamDiscussionsSetting +""" +input UpdateEnterpriseTeamDiscussionsSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the team discussions setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the team discussions setting on the enterprise. + """ + settingValue: EnterpriseEnabledDisabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTeamDiscussionsSetting +""" +type UpdateEnterpriseTeamDiscussionsSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated team discussions setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the team discussions setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateEnterpriseTwoFactorAuthenticationRequiredSetting +""" +input UpdateEnterpriseTwoFactorAuthenticationRequiredSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the enterprise on which to set the two factor authentication required setting. + """ + enterpriseId: ID! @possibleTypes(concreteTypes: ["Enterprise"]) + + """ + The value for the two factor authentication required setting on the enterprise. + """ + settingValue: EnterpriseEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateEnterpriseTwoFactorAuthenticationRequiredSetting +""" +type UpdateEnterpriseTwoFactorAuthenticationRequiredSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The enterprise with the updated two factor authentication required setting. + """ + enterprise: Enterprise + + """ + A message confirming the result of updating the two factor authentication required setting. + """ + message: String +} + +""" +Autogenerated input type of UpdateIpAllowListEnabledSetting +""" +input UpdateIpAllowListEnabledSettingInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the owner on which to set the IP allow list enabled setting. + """ + ownerId: ID! @possibleTypes(concreteTypes: ["Enterprise", "Organization"], abstractType: "IpAllowListOwner") + + """ + The value for the IP allow list enabled setting. + """ + settingValue: IpAllowListEnabledSettingValue! +} + +""" +Autogenerated return type of UpdateIpAllowListEnabledSetting +""" +type UpdateIpAllowListEnabledSettingPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list owner on which the setting was updated. + """ + owner: IpAllowListOwner +} + +""" +Autogenerated input type of UpdateIpAllowListEntry +""" +input UpdateIpAllowListEntryInput { + """ + An IP address or range of addresses in CIDR notation. + """ + allowListValue: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IP allow list entry to update. + """ + ipAllowListEntryId: ID! @possibleTypes(concreteTypes: ["IpAllowListEntry"]) + + """ + Whether the IP allow list entry is active when an IP allow list is enabled. + """ + isActive: Boolean! + + """ + An optional name for the IP allow list entry. + """ + name: String +} + +""" +Autogenerated return type of UpdateIpAllowListEntry +""" +type UpdateIpAllowListEntryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The IP allow list entry that was updated. + """ + ipAllowListEntry: IpAllowListEntry +} + +""" +Autogenerated input type of UpdateIssueComment +""" +input UpdateIssueCommentInput { + """ + The updated text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the IssueComment to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["IssueComment"]) +} + +""" +Autogenerated return type of UpdateIssueComment +""" +type UpdateIssueCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + issueComment: IssueComment +} + +""" +Autogenerated input type of UpdateIssue +""" +input UpdateIssueInput { + """ + An array of Node IDs of users for this issue. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The body for the issue description. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the Issue to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["Issue"]) + + """ + An array of Node IDs of labels for this issue. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + The Node ID of the milestone for this issue. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this issue. + """ + projectIds: [ID!] + + """ + The desired issue state. + """ + state: IssueState + + """ + The title for the issue. + """ + title: String +} + +""" +Autogenerated return type of UpdateIssue +""" +type UpdateIssuePayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The issue. + """ + issue: Issue +} + +""" +Autogenerated input type of UpdateLabel +""" +input UpdateLabelInput @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A 6 character hex code, without the leading #, identifying the updated color of the label. + """ + color: String + + """ + A brief description of the label, such as its purpose. + """ + description: String + + """ + The Node ID of the label to be updated. + """ + id: ID! @possibleTypes(concreteTypes: ["Label"]) + + """ + The updated name of the label. + """ + name: String +} + +""" +Autogenerated return type of UpdateLabel +""" +type UpdateLabelPayload @preview(toggledBy: "bane-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated label. + """ + label: Label +} + +""" +Autogenerated input type of UpdateProjectCard +""" +input UpdateProjectCardInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether or not the ProjectCard should be archived + """ + isArchived: Boolean + + """ + The note of ProjectCard. + """ + note: String + + """ + The ProjectCard ID to update. + """ + projectCardId: ID! @possibleTypes(concreteTypes: ["ProjectCard"]) +} + +""" +Autogenerated return type of UpdateProjectCard +""" +type UpdateProjectCardPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated ProjectCard. + """ + projectCard: ProjectCard +} + +""" +Autogenerated input type of UpdateProjectColumn +""" +input UpdateProjectColumnInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project column. + """ + name: String! + + """ + The ProjectColumn ID to update. + """ + projectColumnId: ID! @possibleTypes(concreteTypes: ["ProjectColumn"]) +} + +""" +Autogenerated return type of UpdateProjectColumn +""" +type UpdateProjectColumnPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project column. + """ + projectColumn: ProjectColumn +} + +""" +Autogenerated input type of UpdateProject +""" +input UpdateProjectInput { + """ + The description of project. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The name of project. + """ + name: String + + """ + The Project ID to update. + """ + projectId: ID! @possibleTypes(concreteTypes: ["Project"]) + + """ + Whether the project is public or not. + """ + public: Boolean + + """ + Whether the project is open or closed. + """ + state: ProjectState +} + +""" +Autogenerated return type of UpdateProject +""" +type UpdateProjectPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated project. + """ + project: Project +} + +""" +Autogenerated input type of UpdatePullRequest +""" +input UpdatePullRequestInput { + """ + An array of Node IDs of users for this pull request. + """ + assigneeIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The name of the branch you want your changes pulled into. This should be an existing branch + on the current repository. + """ + baseRefName: String + + """ + The contents of the pull request. + """ + body: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + An array of Node IDs of labels for this pull request. + """ + labelIds: [ID!] @possibleTypes(concreteTypes: ["Label"]) + + """ + Indicates whether maintainers can modify the pull request. + """ + maintainerCanModify: Boolean + + """ + The Node ID of the milestone for this pull request. + """ + milestoneId: ID @possibleTypes(concreteTypes: ["Milestone"]) + + """ + An array of Node IDs for projects associated with this pull request. + """ + projectIds: [ID!] + + """ + The Node ID of the pull request. + """ + pullRequestId: ID! @possibleTypes(concreteTypes: ["PullRequest"]) + + """ + The target state of the pull request. + """ + state: PullRequestUpdateState + + """ + The title of the pull request. + """ + title: String +} + +""" +Autogenerated return type of UpdatePullRequest +""" +type UpdatePullRequestPayload { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request. + """ + pullRequest: PullRequest +} + +""" +Autogenerated input type of UpdatePullRequestReviewComment +""" +input UpdatePullRequestReviewCommentInput { + """ + The text of the comment. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the comment to modify. + """ + pullRequestReviewCommentId: ID! @possibleTypes(concreteTypes: ["PullRequestReviewComment"]) +} + +""" +Autogenerated return type of UpdatePullRequestReviewComment +""" +type UpdatePullRequestReviewCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + pullRequestReviewComment: PullRequestReviewComment +} + +""" +Autogenerated input type of UpdatePullRequestReview +""" +input UpdatePullRequestReviewInput { + """ + The contents of the pull request review body. + """ + body: String! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the pull request review to modify. + """ + pullRequestReviewId: ID! @possibleTypes(concreteTypes: ["PullRequestReview"]) +} + +""" +Autogenerated return type of UpdatePullRequestReview +""" +type UpdatePullRequestReviewPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated pull request review. + """ + pullRequestReview: PullRequestReview +} + +""" +Autogenerated input type of UpdateRef +""" +input UpdateRefInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Permit updates of branch Refs that are not fast-forwards? + """ + force: Boolean = false + + """ + The GitObjectID that the Ref shall be updated to target. + """ + oid: GitObjectID! + + """ + The Node ID of the Ref to be updated. + """ + refId: ID! @possibleTypes(concreteTypes: ["Ref"]) +} + +""" +Autogenerated return type of UpdateRef +""" +type UpdateRefPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated Ref. + """ + ref: Ref +} + +""" +Autogenerated input type of UpdateRefs +""" +input UpdateRefsInput @preview(toggledBy: "update-refs-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A list of ref updates. + """ + refUpdates: [RefUpdate!]! + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) +} + +""" +Autogenerated return type of UpdateRefs +""" +type UpdateRefsPayload @preview(toggledBy: "update-refs-preview") { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated input type of UpdateRepository +""" +input UpdateRepositoryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + A new description for the repository. Pass an empty string to erase the existing description. + """ + description: String + + """ + Indicates if the repository should have the issues feature enabled. + """ + hasIssuesEnabled: Boolean + + """ + Indicates if the repository should have the project boards feature enabled. + """ + hasProjectsEnabled: Boolean + + """ + Indicates if the repository should have the wiki feature enabled. + """ + hasWikiEnabled: Boolean + + """ + The URL for a web page about this repository. Pass an empty string to erase the existing URL. + """ + homepageUrl: URI + + """ + The new name of the repository. + """ + name: String + + """ + The ID of the repository to update. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + Whether this repository should be marked as a template such that anyone who + can access it can create new repositories with the same files and directory structure. + """ + template: Boolean +} + +""" +Autogenerated return type of UpdateRepository +""" +type UpdateRepositoryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated repository. + """ + repository: Repository +} + +""" +Autogenerated input type of UpdateSubscription +""" +input UpdateSubscriptionInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new state of the subscription. + """ + state: SubscriptionState! + + """ + The Node ID of the subscribable object to modify. + """ + subscribableId: ID! @possibleTypes(concreteTypes: ["Commit", "Issue", "PullRequest", "Repository", "Team", "TeamDiscussion"], abstractType: "Subscribable") +} + +""" +Autogenerated return type of UpdateSubscription +""" +type UpdateSubscriptionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The input subscribable entity. + """ + subscribable: Subscribable +} + +""" +Autogenerated input type of UpdateTeamDiscussionComment +""" +input UpdateTeamDiscussionCommentInput { + """ + The updated text of the comment. + """ + body: String! + + """ + The current version of the body content. + """ + bodyVersion: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the comment to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussionComment"]) +} + +""" +Autogenerated return type of UpdateTeamDiscussionComment +""" +type UpdateTeamDiscussionCommentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated comment. + """ + teamDiscussionComment: TeamDiscussionComment +} + +""" +Autogenerated input type of UpdateTeamDiscussion +""" +input UpdateTeamDiscussionInput { + """ + The updated text of the discussion. + """ + body: String + + """ + The current version of the body content. If provided, this update operation + will be rejected if the given version does not match the latest version on the server. + """ + bodyVersion: String + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the discussion to modify. + """ + id: ID! @possibleTypes(concreteTypes: ["TeamDiscussion"]) + + """ + If provided, sets the pinned state of the updated discussion. + """ + pinned: Boolean + + """ + The updated title of the discussion. + """ + title: String +} + +""" +Autogenerated return type of UpdateTeamDiscussion +""" +type UpdateTeamDiscussionPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The updated discussion. + """ + teamDiscussion: TeamDiscussion +} + +""" +Autogenerated input type of UpdateTeamReviewAssignment +""" +input UpdateTeamReviewAssignmentInput @preview(toggledBy: "stone-crop-preview") { + """ + The algorithm to use for review assignment + """ + algorithm: TeamReviewAssignmentAlgorithm = ROUND_ROBIN + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Turn on or off review assignment + """ + enabled: Boolean! + + """ + An array of team member IDs to exclude + """ + excludedTeamMemberIds: [ID!] @possibleTypes(concreteTypes: ["User"]) + + """ + The Node ID of the team to update review assignments of + """ + id: ID! @possibleTypes(concreteTypes: ["Team"]) + + """ + Notify the entire team of the PR if it is delegated + """ + notifyTeam: Boolean = true + + """ + The number of team members to assign + """ + teamMemberCount: Int = 1 +} + +""" +Autogenerated return type of UpdateTeamReviewAssignment +""" +type UpdateTeamReviewAssignmentPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The team that was modified + """ + team: Team +} + +""" +Autogenerated input type of UpdateTopics +""" +input UpdateTopicsInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The Node ID of the repository. + """ + repositoryId: ID! @possibleTypes(concreteTypes: ["Repository"]) + + """ + An array of topic names. + """ + topicNames: [String!]! +} + +""" +Autogenerated return type of UpdateTopics +""" +type UpdateTopicsPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Names of the provided topics that are not valid. + """ + invalidTopicNames: [String!] + + """ + The updated repository. + """ + repository: Repository +} + +""" +A user is an individual's account on GitHub that owns repositories and can make new content. +""" +type User implements Actor & Node & PackageOwner & ProfileOwner & ProjectOwner & RepositoryOwner & Sponsorable & UniformResourceLocatable { + """ + Determine if this repository owner has any items that can be pinned to their profile. + """ + anyPinnableItems( + """ + Filter to only a particular kind of pinnable item. + """ + type: PinnableItemType + ): Boolean! + + """ + A URL pointing to the user's public avatar. + """ + avatarUrl( + """ + The size of the resulting square image. + """ + size: Int + ): URI! + + """ + The user's public profile bio. + """ + bio: String + + """ + The user's public profile bio as HTML. + """ + bioHTML: HTML! + + """ + A list of commit comments made by this user. + """ + commitComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): CommitCommentConnection! + + """ + The user's public profile company. + """ + company: String + + """ + The user's public profile company as HTML. + """ + companyHTML: HTML! + + """ + The collection of contributions this user has made to different repositories. + """ + contributionsCollection( + """ + Only contributions made at this time or later will be counted. If omitted, defaults to a year ago. + """ + from: DateTime + + """ + The ID of the organization used to filter contributions. + """ + organizationID: ID + + """ + Only contributions made before and up to and including this time will be + counted. If omitted, defaults to the current time. + """ + to: DateTime + ): ContributionsCollection! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The user's publicly visible profile email. + """ + email: String! + + """ + A list of users the given user is followed by. + """ + followers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FollowerConnection! + + """ + A list of users the given user is following. + """ + following( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FollowingConnection! + + """ + Find gist by repo name. + """ + gist( + """ + The gist name to find. + """ + name: String! + ): Gist + + """ + A list of gist comments made by this user. + """ + gistComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): GistCommentConnection! + + """ + A list of the Gists the user has created. + """ + gists( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for gists returned from the connection + """ + orderBy: GistOrder + + """ + Filters Gists according to privacy. + """ + privacy: GistPrivacy + ): GistConnection! + + """ + True if this user/organization has a GitHub Sponsors listing. + """ + hasSponsorsListing: Boolean! + + """ + The hovercard information for this user in a given context + """ + hovercard( + """ + The ID of the subject to get the hovercard in the context of + """ + primarySubjectId: ID + ): Hovercard! + id: ID! + + """ + The interaction ability settings for this user. + """ + interactionAbility: RepositoryInteractionAbility + + """ + Whether or not this user is a participant in the GitHub Security Bug Bounty. + """ + isBountyHunter: Boolean! + + """ + Whether or not this user is a participant in the GitHub Campus Experts Program. + """ + isCampusExpert: Boolean! + + """ + Whether or not this user is a GitHub Developer Program member. + """ + isDeveloperProgramMember: Boolean! + + """ + Whether or not this user is a GitHub employee. + """ + isEmployee: Boolean! + + """ + Whether or not the user has marked themselves as for hire. + """ + isHireable: Boolean! + + """ + Whether or not this user is a site administrator. + """ + isSiteAdmin: Boolean! + + """ + True if the viewer is sponsored by this user/organization. + """ + isSponsoringViewer: Boolean! + + """ + Whether or not this user is the viewing user. + """ + isViewer: Boolean! + + """ + A list of issue comments made by this user. + """ + issueComments( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issue comments returned from the connection. + """ + orderBy: IssueCommentOrder + ): IssueCommentConnection! + + """ + A list of issues associated with this user. + """ + issues( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Filtering options for issues returned from the connection. + """ + filterBy: IssueFilters + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for issues returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the issues by. + """ + states: [IssueState!] + ): IssueConnection! + + """ + Showcases a selection of repositories and gists that the profile owner has + either curated or that have been selected automatically based on popularity. + """ + itemShowcase: ProfileItemShowcase! + + """ + The user's public profile location. + """ + location: String + + """ + The username used to login. + """ + login: String! + + """ + The user's public profile name. + """ + name: String + + """ + Find an organization by its login that the user belongs to. + """ + organization( + """ + The login of the organization to find. + """ + login: String! + ): Organization + + """ + Verified email addresses that match verified domains for a specified organization the user is a member of. + """ + organizationVerifiedDomainEmails( + """ + The login of the organization to match verified domains from. + """ + login: String! + ): [String!]! + + """ + A list of organizations the user belongs to. + """ + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationConnection! + + """ + A list of packages under the owner. + """ + packages( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Find packages by their names. + """ + names: [String] + + """ + Ordering of the returned packages. + """ + orderBy: PackageOrder = {field: CREATED_AT, direction: DESC} + + """ + Filter registry package by type. + """ + packageType: PackageType + + """ + Find packages in a repository by ID. + """ + repositoryId: ID + ): PackageConnection! + + """ + A list of repositories and gists this profile owner can pin to their profile. + """ + pinnableItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinnable items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + A list of repositories and gists this profile owner has pinned to their profile + """ + pinnedItems( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Filter the types of pinned items that are returned. + """ + types: [PinnableItemType!] + ): PinnableItemConnection! + + """ + Returns how many more items this profile owner can pin to their profile. + """ + pinnedItemsRemaining: Int! + + """ + Find project by number. + """ + project( + """ + The project number to find. + """ + number: Int! + ): Project + + """ + A list of projects under the owner. + """ + projects( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for projects returned from the connection + """ + orderBy: ProjectOrder + + """ + Query to search projects by, currently only searching by name. + """ + search: String + + """ + A list of states to filter the projects by. + """ + states: [ProjectState!] + ): ProjectConnection! + + """ + The HTTP path listing user's projects + """ + projectsResourcePath: URI! + + """ + The HTTP URL listing user's projects + """ + projectsUrl: URI! + + """ + A list of public keys associated with this user. + """ + publicKeys( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PublicKeyConnection! + + """ + A list of pull requests associated with this user. + """ + pullRequests( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + The base ref name to filter the pull requests by. + """ + baseRefName: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + The head ref name to filter the pull requests by. + """ + headRefName: String + + """ + A list of label names to filter the pull requests by. + """ + labels: [String!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for pull requests returned from the connection. + """ + orderBy: IssueOrder + + """ + A list of states to filter the pull requests by. + """ + states: [PullRequestState!] + ): PullRequestConnection! + + """ + A list of repositories that the user owns. + """ + repositories( + """ + Array of viewer's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + current viewer owns. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they are forks of another repository + """ + isFork: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + A list of repositories that the user recently contributed to. + """ + repositoriesContributedTo( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + If non-null, include only the specified types of contributions. The + GitHub.com UI uses [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] + """ + contributionTypes: [RepositoryContributionType] + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If true, include user repositories + """ + includeUserRepositories: Boolean + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + Find Repository. + """ + repository( + """ + Name of Repository to find. + """ + name: String! + ): Repository + + """ + The HTTP path for this user + """ + resourcePath: URI! + + """ + Replies this user has saved + """ + savedReplies( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + The field to order saved replies by. + """ + orderBy: SavedReplyOrder = {field: UPDATED_AT, direction: DESC} + ): SavedReplyConnection + + """ + The GitHub Sponsors listing for this user or organization. + """ + sponsorsListing: SponsorsListing + + """ + This object's sponsorships as the maintainer. + """ + sponsorshipsAsMaintainer( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Whether or not to include private sponsorships in the result set + """ + includePrivate: Boolean = false + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + This object's sponsorships as the sponsor. + """ + sponsorshipsAsSponsor( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for sponsorships returned from this connection. If left + blank, the sponsorships will be ordered based on relevancy to the viewer. + """ + orderBy: SponsorshipOrder + ): SponsorshipConnection! + + """ + Repositories the user has starred. + """ + starredRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Order for connection + """ + orderBy: StarOrder + + """ + Filters starred repositories to only return repositories owned by the viewer. + """ + ownedByViewer: Boolean + ): StarredRepositoryConnection! + + """ + The user's description of what they're currently doing. + """ + status: UserStatus + + """ + Repositories the user has contributed to, ordered by contribution rank, plus repositories the user has created + """ + topRepositories( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder! + + """ + How far back in time to fetch contributed repositories + """ + since: DateTime + ): RepositoryConnection! + + """ + The user's Twitter username. + """ + twitterUsername: String + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The HTTP URL for this user + """ + url: URI! + + """ + Can the viewer pin repositories and gists to the profile? + """ + viewerCanChangePinnedItems: Boolean! + + """ + Can the current viewer create new projects on this owner. + """ + viewerCanCreateProjects: Boolean! + + """ + Whether or not the viewer is able to follow the user. + """ + viewerCanFollow: Boolean! + + """ + Whether or not the viewer is able to sponsor this user/organization. + """ + viewerCanSponsor: Boolean! + + """ + Whether or not this user is followed by the viewer. + """ + viewerIsFollowing: Boolean! + + """ + True if the viewer is sponsoring this user/organization. + """ + viewerIsSponsoring: Boolean! + + """ + A list of repositories the given user is watching. + """ + watching( + """ + Affiliation options for repositories returned from the connection. If none + specified, the results will include repositories for which the current + viewer is an owner or collaborator, or member. + """ + affiliations: [RepositoryAffiliation] + + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + If non-null, filters repositories according to whether they have been locked + """ + isLocked: Boolean + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + Ordering options for repositories returned from the connection + """ + orderBy: RepositoryOrder + + """ + Array of owner's affiliation options for repositories returned from the + connection. For example, OWNER will include only repositories that the + organization or user being viewed owns. + """ + ownerAffiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + """ + If non-null, filters repositories according to privacy + """ + privacy: RepositoryPrivacy + ): RepositoryConnection! + + """ + A URL pointing to the user's public website/blog. + """ + websiteUrl: URI +} + +""" +The possible durations that a user can be blocked for. +""" +enum UserBlockDuration { + """ + The user was blocked for 1 day + """ + ONE_DAY + + """ + The user was blocked for 30 days + """ + ONE_MONTH + + """ + The user was blocked for 7 days + """ + ONE_WEEK + + """ + The user was blocked permanently + """ + PERMANENT + + """ + The user was blocked for 3 days + """ + THREE_DAYS +} + +""" +Represents a 'user_blocked' event on a given user. +""" +type UserBlockedEvent implements Node { + """ + Identifies the actor who performed the event. + """ + actor: Actor + + """ + Number of days that the user was blocked for. + """ + blockDuration: UserBlockDuration! + + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + id: ID! + + """ + The user who was blocked. + """ + subject: User +} + +""" +The connection type for User. +""" +type UserConnection { + """ + A list of edges. + """ + edges: [UserEdge] + + """ + A list of nodes. + """ + nodes: [User] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edit on user content +""" +type UserContentEdit implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + Identifies the date and time when the object was deleted. + """ + deletedAt: DateTime + + """ + The actor who deleted this content + """ + deletedBy: Actor + + """ + A summary of the changes for this edit + """ + diff: String + + """ + When this content was edited + """ + editedAt: DateTime! + + """ + The actor who edited this content + """ + editor: Actor + id: ID! + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! +} + +""" +A list of edits to content. +""" +type UserContentEditConnection { + """ + A list of edges. + """ + edges: [UserContentEditEdge] + + """ + A list of nodes. + """ + nodes: [UserContentEdit] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserContentEditEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserContentEdit +} + +""" +Represents a user. +""" +type UserEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: User +} + +""" +Email attributes from External Identity +""" +type UserEmailMetadata { + """ + Boolean to identify primary emails + """ + primary: Boolean + + """ + Type of email + """ + type: String + + """ + Email id + """ + value: String! +} + +""" +The user's description of what they're currently doing. +""" +type UserStatus implements Node { + """ + Identifies the date and time when the object was created. + """ + createdAt: DateTime! + + """ + An emoji summarizing the user's status. + """ + emoji: String + + """ + The status emoji as HTML. + """ + emojiHTML: HTML + + """ + If set, the status will not be shown after this date. + """ + expiresAt: DateTime + + """ + ID of the object. + """ + id: ID! + + """ + Whether this status indicates the user is not fully available on GitHub. + """ + indicatesLimitedAvailability: Boolean! + + """ + A brief message describing what the user is doing. + """ + message: String + + """ + The organization whose members can see this status. If null, this status is publicly visible. + """ + organization: Organization + + """ + Identifies the date and time when the object was last updated. + """ + updatedAt: DateTime! + + """ + The user who has this status. + """ + user: User! +} + +""" +The connection type for UserStatus. +""" +type UserStatusConnection { + """ + A list of edges. + """ + edges: [UserStatusEdge] + + """ + A list of nodes. + """ + nodes: [UserStatus] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UserStatusEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UserStatus +} + +""" +Ordering options for user status connections. +""" +input UserStatusOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order user statuses by. + """ + field: UserStatusOrderField! +} + +""" +Properties by which user status connections can be ordered. +""" +enum UserStatusOrderField { + """ + Order user statuses by when they were updated. + """ + UPDATED_AT +} + +""" +A domain that can be verified for an organization or an enterprise. +""" +type VerifiableDomain implements Node { + """ + Identifies the primary key from the database. + """ + databaseId: Int + + """ + The DNS host name that should be used for verification. + """ + dnsHostName: URI + + """ + The unicode encoded domain. + """ + domain: URI! + + """ + Whether a TXT record for verification with the expected host name was found. + """ + hasFoundHostName: Boolean! + + """ + Whether a TXT record for verification with the expected verification token was found. + """ + hasFoundVerificationToken: Boolean! + id: ID! + + """ + Whether this domain is required to exist for an organization policy to be enforced. + """ + isRequiredForPolicyEnforcement: Boolean! + + """ + Whether or not the domain is verified. + """ + isVerified: Boolean! + + """ + The owner of the domain. + """ + owner: VerifiableDomainOwner! + + """ + The punycode encoded domain. + """ + punycodeEncodedDomain: URI! + + """ + The time that the current verification token will expire. + """ + tokenExpirationTime: DateTime + + """ + The current verification token for the domain. + """ + verificationToken: String +} + +""" +The connection type for VerifiableDomain. +""" +type VerifiableDomainConnection { + """ + A list of edges. + """ + edges: [VerifiableDomainEdge] + + """ + A list of nodes. + """ + nodes: [VerifiableDomain] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + Identifies the total count of items in the connection. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type VerifiableDomainEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: VerifiableDomain +} + +""" +Ordering options for verifiable domain connections. +""" +input VerifiableDomainOrder { + """ + The ordering direction. + """ + direction: OrderDirection! + + """ + The field to order verifiable domains by. + """ + field: VerifiableDomainOrderField! +} + +""" +Properties by which verifiable domain connections can be ordered. +""" +enum VerifiableDomainOrderField { + """ + Order verifiable domains by the domain name. + """ + DOMAIN +} + +""" +Types that can own a verifiable domain. +""" +union VerifiableDomainOwner = Enterprise | Organization + +""" +Autogenerated input type of VerifyVerifiableDomain +""" +input VerifyVerifiableDomainInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The ID of the verifiable domain to verify. + """ + id: ID! @possibleTypes(concreteTypes: ["VerifiableDomain"]) +} + +""" +Autogenerated return type of VerifyVerifiableDomain +""" +type VerifyVerifiableDomainPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The verifiable domain that was verified. + """ + domain: VerifiableDomain +} + +""" +A hovercard context with a message describing how the viewer is related. +""" +type ViewerHovercardContext implements HovercardContext { + """ + A string describing this context + """ + message: String! + + """ + An octicon to accompany this context + """ + octicon: String! + + """ + Identifies the user who is related to this context. + """ + viewer: User! +} + +""" +A valid x509 certificate string +""" +scalar X509Certificate diff --git a/cla-backend-go/github/github_installation.go b/cla-backend-go/github/github_installation.go index e609694c2..79f845ff2 100644 --- a/cla-backend-go/github/github_installation.go +++ b/cla-backend-go/github/github_installation.go @@ -11,7 +11,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" - "github.com/google/go-github/v33/github" + "github.com/google/go-github/v37/github" log "github.com/communitybridge/easycla/cla-backend-go/logging" ) @@ -36,19 +36,19 @@ func GetInstallationRepositories(ctx context.Context, installationID int64) ([]* // See pagination examples: https://godoc.org/github.com/google/go-github/github opts := &github.ListOptions{ - PerPage: 50, + PerPage: 100, // Max 100 per the GitHub API } for { - repos, resp, err := client.Apps.ListRepos(ctx, opts) + listReposResponse, resp, err := client.Apps.ListRepos(ctx, opts) if err != nil { msg := fmt.Sprintf("error while getting repositories associated for installation, error: %+v", err) log.WithFields(f).WithError(err).Warn(msg) return nil, errors.New(msg) } - log.WithFields(f).Debugf("fetched %d records...", len(repos)) - allRepos = append(allRepos, repos...) + //log.WithFields(f).Debugf("fetched %d records...", len(listReposResponse.Repositories)) + allRepos = append(allRepos, listReposResponse.Repositories...) if resp.NextPage == 0 { break } diff --git a/cla-backend-go/github/github_org.go b/cla-backend-go/github/github_org.go index 3ba8774fc..b8b0c3a2d 100644 --- a/cla-backend-go/github/github_org.go +++ b/cla-backend-go/github/github_org.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/google/go-github/v33/github" + "github.com/google/go-github/v37/github" ) // errors @@ -20,6 +20,27 @@ var ( ErrGithubOrganizationNotFound = errors.New("github organization name not found") ) +// GetOrganization gets github organization +func GetMembership(ctx context.Context, user, organizationName string) (*github.Membership, error) { + f := logrus.Fields{ + "functionName": "GetOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "organizationName": organizationName, + } + + client := NewGithubOauthClient() + membership, resp, err := client.Organizations.GetOrgMembership(ctx, user, organizationName) + + if err != nil { + log.WithFields(f).Warnf("GetOrgOrganization %s failed. error = %s", organizationName, err.Error()) + if resp != nil && resp.StatusCode == 404 { + return nil, ErrGithubOrganizationNotFound + } + return nil, err + } + return membership, nil +} + // GetOrganization gets github organization func GetOrganization(ctx context.Context, organizationName string) (*github.Organization, error) { f := logrus.Fields{ @@ -44,3 +65,33 @@ func GetOrganization(ctx context.Context, organizationName string) (*github.Orga } return org, nil } + +// GetOrganizationMembers gets members in organization +func GetOrganizationMembers(ctx context.Context, orgName string, installationID int64) ([]string, error) { + f := logrus.Fields{ + "functionName": "GetOrganizationMembers", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + client, err := NewGithubAppClient(installationID) + if err != nil { + msg := fmt.Sprintf("unable to create a github client, error: %+v", err) + log.WithFields(f).WithError(err).Warn(msg) + return nil, errors.New(msg) + } + + users, resp, err := client.Organizations.ListMembers(ctx, orgName, nil) + + if resp.StatusCode < 200 || resp.StatusCode > 299 || err != nil { + msg := fmt.Sprintf("List Org Members failed for Organization: %s with no success response code %d. error = %s", orgName, resp.StatusCode, err.Error()) + log.WithFields(f).Warnf(msg) + return nil, errors.New(msg) + } + + var ghUsernames []string + for _, user := range users { + log.WithFields(f).Debugf("user :%s found for organization: %s", *user.Login, orgName) + ghUsernames = append(ghUsernames, *user.Login) + } + return ghUsernames, nil +} diff --git a/cla-backend-go/github/github_repository.go b/cla-backend-go/github/github_repository.go index 268da0859..651798a6e 100644 --- a/cla-backend-go/github/github_repository.go +++ b/cla-backend-go/github/github_repository.go @@ -7,20 +7,548 @@ import ( "context" "errors" "fmt" + "net/http" + "strconv" + "strings" + log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" - log "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/google/go-github/v33/github" + "github.com/google/go-github/v37/github" ) -// errors var ( - ErrGithubRepositoryNotFound = errors.New("github repository not found") + // ErrGitHubRepositoryNotFound is returned when github repository is not found + ErrGitHubRepositoryNotFound = errors.New("github repository not found") ) +const ( + help = "https://help.github.com/en/github/committing-changes-to-your-project/why-are-my-commits-linked-to-the-wrong-user" + unknown = "Unknown" + failureState = "failure" + successState = "success" +) + +func GetGitHubRepository(ctx context.Context, installationID, githubRepositoryID int64) (*github.Repository, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.GetGitHubRepository", + "installationID": installationID, + "githubRepositoryID": githubRepositoryID, + } + client, clientErr := NewGithubAppClient(installationID) + if clientErr != nil { + log.WithFields(f).WithError(clientErr).Warnf("problem loading github client for installation ID: %d", installationID) + return nil, clientErr + } + + log.WithFields(f).Debugf("getting github repository by id: %d", githubRepositoryID) + repository, httpResponse, repoErr := client.Repositories.GetByID(ctx, githubRepositoryID) + if repoErr != nil { + log.WithFields(f).WithError(repoErr).Warnf("unable to fetch repository by ID: %d", githubRepositoryID) + return nil, repoErr + } + if httpResponse.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("unexpected status code: %d", httpResponse.StatusCode) + return nil, ErrGitHubRepositoryNotFound + } + + //log.WithFields(f).Debugf("successfully retrieved github repository by id: %d - repository object: %+v", githubRepositoryID, repository) + return repository, nil +} + +func GetPullRequest(ctx context.Context, pullRequestID int, owner, repo string, client *github.Client) (*github.PullRequest, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.GetPullRequest", + "pullRequestID": pullRequestID, + "owner": owner, + "repo": repo, + } + + pullRequest, _, err := client.PullRequests.Get(ctx, owner, repo, pullRequestID) + if err != nil { + logging.WithFields(f).WithError(err).Warn("unable to get pull request") + return nil, err + } + + return pullRequest, nil +} + +// UserCommitSummary data model +type UserCommitSummary struct { + SHA string + CommitAuthor *github.User + Affiliated bool + Authorized bool +} + +// GetCommitAuthorID commit author username ID (numeric value as a string) if available, otherwise returns empty string +func (u UserCommitSummary) GetCommitAuthorID() string { + if u.CommitAuthor != nil && u.CommitAuthor.ID != nil { + return strconv.Itoa(int(*u.CommitAuthor.ID)) + } + + return "" +} + +// GetCommitAuthorUsername returns commit author username if available, otherwise returns empty string +func (u UserCommitSummary) GetCommitAuthorUsername() string { + if u.CommitAuthor != nil { + if u.CommitAuthor.Login != nil { + return *u.CommitAuthor.Login + } + if u.CommitAuthor.Name != nil { + return *u.CommitAuthor.Name + } + } + + return "" +} + +// GetCommitAuthorEmail returns commit author email if available, otherwise returns empty string +func (u UserCommitSummary) GetCommitAuthorEmail() string { + if u.CommitAuthor != nil && u.CommitAuthor.Email != nil { + return *u.CommitAuthor.Email + } + + return "" +} + +// IsValid returns true if the commit author information is available +func (u UserCommitSummary) IsValid() bool { + valid := false + if u.CommitAuthor != nil { + valid = u.CommitAuthor.ID != nil && (u.CommitAuthor.Login != nil || u.CommitAuthor.Name != nil) + } + return valid +} + +// GetDisplayText returns the display text for the user commit summary +func (u UserCommitSummary) GetDisplayText(tagUser bool) string { + if !u.IsValid() { + return "Invalid author details.\n" + } + if u.Affiliated && u.Authorized { + return fmt.Sprintf("%s is authorized.\n ", u.getUserInfo(tagUser)) + } + if u.Affiliated { + return fmt.Sprintf("%s is associated with a company, but not an approval list.\n", u.getUserInfo(tagUser)) + } else { + return fmt.Sprintf("%s is not associated with a company.\n", u.getUserInfo(tagUser)) + } +} + +func (u UserCommitSummary) getUserInfo(tagUser bool) string { + + f := logrus.Fields{ + "functionName": "github.github_repository.getUserInfo", + "tagUser": tagUser, + } + + userInfo := "" + tagValue := "" + var sb strings.Builder + sb.WriteString(userInfo) + + log.WithFields(f).Debugf("author: %+v", u.CommitAuthor) + + if tagUser { + tagValue = "@" + } + if u.CommitAuthor != nil { + if *u.CommitAuthor.Login != "" { + sb.WriteString(fmt.Sprintf("login: %s%s / ", tagValue, *u.CommitAuthor.Login)) + } + + if u.CommitAuthor.Name != nil { + sb.WriteString(fmt.Sprintf("%sname: %s / ", userInfo, utils.StringValue(u.CommitAuthor.Name))) + } + } + + return strings.Replace(sb.String(), "/ $", "", -1) +} + +func GetPullRequestCommitAuthors(ctx context.Context, installationID int64, pullRequestID int, owner, repo string) ([]*UserCommitSummary, *string, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.GetPullRequestCommitAuthors", + "pullRequestID": pullRequestID, + } + var userCommitSummary []*UserCommitSummary + + client, err := NewGithubAppClient(installationID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to create Github client") + return nil, nil, err + } + + commits, resp, comErr := client.PullRequests.ListCommits(ctx, owner, repo, pullRequestID, &github.ListOptions{}) + if comErr != nil { + log.WithFields(f).WithError(comErr).Warnf("problem listing commits for repo: %s/%s pull request: %d", owner, repo, pullRequestID) + return nil, nil, comErr + } + if resp.StatusCode != http.StatusOK { + msg := fmt.Sprintf("unexpected status code: %d - expected: %d", resp.StatusCode, http.StatusOK) + log.WithFields(f).Warn(msg) + return nil, nil, errors.New(msg) + } + + log.WithFields(f).Debugf("found %d commits for pull request: %d", len(commits), pullRequestID) + for _, commit := range commits { + log.WithFields(f).Debugf("loaded commit: %+v", commit) + commitAuthor := "" + if commit.Commit != nil && commit.Commit.Author != nil && commit.Commit.Author.Login != nil { + log.WithFields(f).Debugf("commit.Commit.Author: %s", utils.StringValue(commit.Commit.Author.Login)) + commitAuthor = utils.StringValue(commit.Commit.Author.Login) + } else if commit.Author != nil && commit.Author.Login != nil { + log.WithFields(f).Debugf("commit.Author.Login: %s", utils.StringValue(commit.Author.Login)) + commitAuthor = utils.StringValue(commit.Author.Login) + } + log.WithFields(f).Debugf("commitAuthor: %s", commitAuthor) + userCommitSummary = append(userCommitSummary, &UserCommitSummary{ + SHA: *commit.SHA, + CommitAuthor: commit.Author, + Affiliated: false, + Authorized: false, + }) + } + + // get latest commit SHA + latestCommitSHA := commits[len(commits)-1].SHA + return userCommitSummary, latestCommitSHA, nil +} + +func UpdatePullRequest(ctx context.Context, installationID int64, pullRequestID int, owner, repo string, repoID *int64, latestSHA string, signed []*UserCommitSummary, missing []*UserCommitSummary, CLABaseAPIURL, CLALandingPage, CLALogoURL string) error { + f := logrus.Fields{ + "functionName": "github.github_repository.UpdatePullRequest", + "installationID": installationID, + "owner": owner, + "repo": repo, + "SHA": latestSHA, + "pullRequestID": pullRequestID, + } + + client, err := NewGithubAppClient(installationID) + if err != nil || client == nil { + log.WithFields(f).WithError(err).Warn("unable to create Github client") + return err + } + + // Update comments as necessary + log.WithFields(f).Debugf("updating comment for PR: %d... ", pullRequestID) + + previouslyFailed, comment, failedErr := hasCheckPreviouslyFailed(ctx, client, owner, repo, pullRequestID) + if failedErr != nil { + log.WithFields(f).WithError(failedErr).Debugf("unable to check previously failed PR: %d", pullRequestID) + return failedErr + } + + previouslySucceeded, previousSucceededComment, succeedErr := hasCheckPreviouslySucceeded(ctx, client, owner, repo, pullRequestID) + if succeedErr != nil { + log.WithFields(f).WithError(succeedErr).Debugf("unable to check previously succeeded PR: %d", pullRequestID) + return failedErr + } + + body := assembleCLAComment(ctx, int(installationID), pullRequestID, repoID, signed, missing, CLABaseAPIURL, CLALogoURL, CLALandingPage) + + if len(missing) == 0 { + // All contributors are passing + + // If we have previously failed, we need to update the comment + if previouslyFailed { + log.WithFields(f).Debugf("Found previously failed checks - updating the CLA comment in the PR : %d", pullRequestID) + comment.Body = &body + _, _, err = client.Issues.EditComment(ctx, owner, repo, *comment.ID, comment) + if err != nil { + log.WithFields(f).Debug("unable to edit comment ") + return err + } + } + } else { + // One or more contributors are failing + + // If we have previously failed, we need to update the comment + if previouslyFailed { + log.WithFields(f).Debugf("Found previously failed checks - updating the CLA comment in the PR : %d", pullRequestID) + comment.Body = &body + _, _, err = client.Issues.EditComment(ctx, owner, repo, *comment.ID, comment) + if err != nil { + log.WithFields(f).Debug("unable to edit comment ") + return err + } + } else if previouslySucceeded { + // If we have previously succeeded, then we also need to update the comment (pass => fail) + log.WithFields(f).Debugf("Found previously succeeeded checks - updating the CLA comment in the PR : %d", pullRequestID) + // Generate a new comment with all the failed CLA info + failedComment := assembleCLAComment(ctx, int(installationID), pullRequestID, repoID, signed, missing, CLABaseAPIURL, CLALogoURL, CLALandingPage) + previousSucceededComment.Body = &failedComment + _, _, err = client.Issues.EditComment(ctx, owner, repo, *previousSucceededComment.ID, previousSucceededComment) + if err != nil { + log.WithFields(f).Debug("unable to edit comment ") + return err + } + } else { + // no previous comment - need to create a new comment + _, _, err = client.Issues.CreateComment(ctx, owner, repo, pullRequestID, comment) + if err != nil { + log.WithFields(f).Debug("unable to create comment") + } + + log.WithFields(f).Debugf(`EasyCLA App checks fail for PR: %d. + CLA signatures with signed authors: %+v and with missing authors: %+v`, pullRequestID, signed, missing) + } + } + + // Update/Create the status + context := "EasyCLA" + var statusBody string + var state string + var signURL string + + if len(missing) > 0 { + state = failureState + context, statusBody = assembleCLAStatus(context, false) + signURL = getFullSignURL("github", strconv.Itoa(int(installationID)), strconv.Itoa(int(*repoID)), strconv.Itoa(pullRequestID), CLABaseAPIURL) + log.WithFields(f).Debugf("Creating new CLA %s status - %d passed, %d missing, signing url %s", state, len(signed), len(missing), signURL) + } else if len(signed) > 0 { + state = successState + context, statusBody = assembleCLAStatus(context, true) + signURL = fmt.Sprintf("%s/#/?version=2", CLALandingPage) + log.WithFields(f).Debugf("Creating new CLA %s status - %d passed, %d missing, signing url %s", state, len(signed), len(missing), signURL) + + } else { + state = failureState + context, statusBody = assembleCLAStatus(context, false) + signURL = getFullSignURL("github", strconv.Itoa(int(installationID)), strconv.Itoa(int(*repoID)), strconv.Itoa(pullRequestID), CLABaseAPIURL) + log.WithFields(f).Debugf("Creating new CLA %s status - %d passed, %d missing, signing url %s", state, len(signed), len(missing), signURL) + log.WithFields(f).Debugf("This is an error condition - should have at least one committer in one of these lists: signed : %+v passed, %+v", signed, missing) + } + + status := Status{ + State: &state, + TargetURL: &signURL, + Context: &context, + Description: &statusBody, + } + + log.WithFields(f).Debugf("Creating status: %+v", status) + + _, _, err = CreateStatus(ctx, client, owner, repo, latestSHA, &status) + if err != nil { + log.WithFields(f).Debugf("unable to create status: %v", status) + return err + } + + return nil +} + +func hasCheckPreviouslyFailed(ctx context.Context, client *github.Client, owner, repo string, pullRequestID int) (bool, *github.IssueComment, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.hasCheckPreviouslyFailed", + } + + comments, _, err := client.Issues.ListComments(ctx, owner, repo, pullRequestID, &github.IssueListCommentsOptions{}) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get fetch comments for repo: %s, pr: %d", repo, pullRequestID) + return false, nil, err + } + + for _, comment := range comments { + if strings.Contains(*comment.Body, "is not authorized under a signed CLA") { + return true, comment, nil + } + if strings.Contains(*comment.Body, "they must confirm their affiliation") { + return true, comment, nil + } + if strings.Contains(*comment.Body, "is missing the User") { + return true, comment, nil + } + } + return false, nil, nil +} + +func hasCheckPreviouslySucceeded(ctx context.Context, client *github.Client, owner, repo string, pullRequestID int) (bool, *github.IssueComment, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.hasCheckPreviouslySucceeded", + } + + comments, _, err := client.Issues.ListComments(ctx, owner, repo, pullRequestID, &github.IssueListCommentsOptions{}) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get fetch comments for repo: %s, pr: %d", repo, pullRequestID) + return false, nil, err + } + + for _, comment := range comments { + if strings.Contains(*comment.Body, "The committers listed above are authorized under a signed CLA.") { + return true, comment, nil + } + } + + return false, nil, nil +} + +func assembleCLAStatus(authorName string, signed bool) (string, string) { + if authorName == "" { + authorName = unknown + } + if signed { + return authorName, "EasyCLA check passed. You are authorized to contribute." + } + return authorName, "Missing CLA Authorization." +} + +func assembleCLAComment(ctx context.Context, installationID, pullRequestID int, repositoryID *int64, signed, missing []*UserCommitSummary, apiBaseURL, CLALogoURL, CLALandingPage string) string { + f := logrus.Fields{ + "functionName": "github.github_repository.assembleCLAComment", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "installationID": installationID, + "repositoryID": repositoryID, + "pullRequestID": pullRequestID, + "repoID": *repositoryID, + } + + repositoryType := "github" + missingID := false + for _, userSummary := range missing { + if userSummary.GetCommitAuthorID() == "" { + missingID = true + } + } + + log.WithFields(f).Debug("Building CLAComment body ") + signURL := getFullSignURL(repositoryType, strconv.Itoa(installationID), strconv.Itoa(int(*repositoryID)), strconv.Itoa(pullRequestID), apiBaseURL) + commentBody := getCommentBody(repositoryType, signURL, signed, missing) + allSigned := len(missing) == 0 + badge := getCommentBadge(allSigned, signURL, missingID, false, CLALandingPage, CLALogoURL) + return fmt.Sprintf("%s
    %s", badge, commentBody) +} + +func getCommentBody(repositoryType, signURL string, signed, missing []*UserCommitSummary) string { + f := logrus.Fields{ + "functionName": "github.github_repository:getCommentBody", + "repositoryType": repositoryType, + "signURL": signURL, + } + + failed := ":x:" + success := ":white_check_mark:" + committersComment := strings.Builder{} + text := "" + + if len(missing) > 0 || len(signed) > 0 { + committersComment.WriteString("
      ") + } + + if len(signed) > 0 { + committers := getAuthorInfoCommits(signed, false) + + for k, v := range committers { + var shas []string + for _, summary := range v { + shas = append(shas, summary.SHA) + log.WithFields(f).Debugf("SHAS for signed users: %s", shas) + committersComment.WriteString(fmt.Sprintf("
    • %s%s(%s)
    • ", success, k, strings.Join(shas, ", "))) + } + } + } + + if len(missing) > 0 { + log.WithFields(f).Debugf("processing %d missing contributors", len(missing)) + supportURL := "https://jira.linuxfoundation.org/servicedesk/customer/portal/4" + committers := getAuthorInfoCommits(missing, true) + helpURL := help + + for k, v := range committers { + var shas []string + for _, summary := range v { + shas = append(shas, summary.SHA) + } + if k == unknown { + committersComment.WriteString(fmt.Sprintf(`
    • %s The commit (%s). This user is missing the User's ID, preventing the EasyCLA check. Consult GitHub Help to resolve. For further assistance with EasyCLA, please submit a support request ticket.
    • `, + failed, strings.Join(shas, ", "), helpURL, supportURL)) + } else { + var missingAffiliations []*UserCommitSummary + for _, summary := range v { + if !summary.Affiliated && !summary.Authorized { + missingAffiliations = append(missingAffiliations, summary) + } + } + if len(missingAffiliations) > 0 { + log.WithFields(f).Debugf("SHAs for users with missing company affiliations: %+v", shas) + committersComment.WriteString( + fmt.Sprintf(`
    • %s %s The commit (%s). This user is authorized, but they must confirm their affiliation with their company. Start the authorization process by clicking here, click \"Corporate\", select the appropriate company from the list, then confirm your affiliation on the page that appears. For further assistance with EasyCLA, please submit a support request ticket.
    • `, + failed, k, strings.Join(shas, ", "), signURL, supportURL)) + } else { + committersComment.WriteString( + fmt.Sprintf(`
    • %s - %s The commit (%s) is not authorized under a signed CLA. "Please click here to be authorized. For further assistance with EasyCLA, please submit a support request ticket.
    • `, + signURL, failed, k, strings.Join(shas, ", "), signURL, supportURL)) + } + } + } + } + + if len(signed) > 0 || len(missing) > 0 { + committersComment.WriteString("
    ") + } + + if len(signed) > 0 && len(missing) == 0 { + text = "
    The committers listed above are authorized under a signed CLA." + } + + return fmt.Sprintf("%s%s", committersComment.String(), text) +} + +func getCommentBadge(allSigned bool, signURL string, missingUserId, managerApproved bool, CLALandingPage, CLALogoURL string) string { + var alt string + var text string + var badgeHyperLink string + var badgeURL string + + if allSigned { + badgeURL = fmt.Sprintf("%s/cla-signed.svg", CLALogoURL) + badgeHyperLink = fmt.Sprintf("%s/#/?version=2", CLALandingPage) + alt = "CLA Signed" + return fmt.Sprintf(`%s`, badgeHyperLink, badgeURL, alt) + } + badgeHyperLink = signURL + if missingUserId { + badgeURL = fmt.Sprintf("%s/cla-missing-id.svg", CLALogoURL) + alt = "CLA Missing ID" + } else if managerApproved { + badgeURL = fmt.Sprintf("%s/cla-confirmation-needed.svg", CLALogoURL) + alt = "CLA Confirmation Needed" + } else { + badgeURL = fmt.Sprintf("%s/cla-not-signed.svg", CLALogoURL) + alt = "CLA Not Signed" + } + + text = fmt.Sprintf(`%s`, badgeHyperLink, badgeURL, alt) + return fmt.Sprintf("%s
    ", text) +} + +func getFullSignURL(repositoryType, installationID, githubRepositoryID, pullRequestID, apiBaseURL string) string { + return fmt.Sprintf("%s/v2/repository-provider/%s/sign/%s/%s/%s/#/?version=2", apiBaseURL, repositoryType, installationID, githubRepositoryID, pullRequestID) +} + +func getAuthorInfoCommits(userSummary []*UserCommitSummary, tagUser bool) map[string][]*UserCommitSummary { + f := logrus.Fields{ + "functioName": "github.github_repository.getAuthorInfoCommits", + } + result := make(map[string][]*UserCommitSummary) + for _, author := range userSummary { + log.WithFields(f).WithFields(f).Debugf("checking user summary for : %s", author.getUserInfo(tagUser)) + if _, ok := result[author.getUserInfo(tagUser)]; !ok { + + result[author.getUserInfo(tagUser)] = []*UserCommitSummary{ + author, + } + } else { + result[author.getUserInfo(tagUser)] = append(result[author.getUserInfo(tagUser)], author) + } + } + return result +} + // GetRepositoryByExternalID finds github repository by github repository id func GetRepositoryByExternalID(ctx context.Context, installationID, id int64) (*github.Repository, error) { client, err := NewGithubAppClient(installationID) @@ -29,9 +557,9 @@ func GetRepositoryByExternalID(ctx context.Context, installationID, id int64) (* } org, resp, err := client.Repositories.GetByID(ctx, id) if err != nil { - logging.Warnf("GetRepository %v failed. error = %s", id, err.Error()) + logging.Warnf("GitHubGetRepository %v failed. error = %s", id, err.Error()) if resp.StatusCode == 404 { - return nil, ErrGithubRepositoryNotFound + return nil, ErrGitHubRepositoryNotFound } return nil, err } @@ -90,3 +618,63 @@ func GetRepositories(ctx context.Context, organizationName string) ([]*github.Re return responseRepoList, nil } + +type Status struct { + State *string `json:"state,omitempty"` + TargetURL *string `json:"target_url,omitempty"` + Description *string `json:"description,omitempty"` + Context *string `json:"context,omitempty"` +} + +// CreateStatus creates a new status on the specified commit. +// +// GitHub API docs:https://docs.github.com/en/rest/commits/statuses +func CreateStatus(ctx context.Context, client *github.Client, owner, repo, sha string, status *Status) (*Status, *github.Response, error) { + u := fmt.Sprintf("repos/%v/%v/statuses/%v", owner, repo, sha) + req, err := client.NewRequest("POST", u, status) + if err != nil { + return nil, nil, err + } + c := new(Status) + resp, err := client.Do(ctx, req, c) + if err != nil { + return nil, resp, err + } + + return c, resp, nil +} + +func GetReturnURL(ctx context.Context, installationID, repositoryID int64, pullRequestID int) (string, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.GetReturnURL", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "installationID": installationID, + "repositoryID": repositoryID, + "pullRequestID": pullRequestID, + } + + client, err := NewGithubAppClient(installationID) + + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to create Github client") + return "", err + } + + log.WithFields(f).Debugf("getting github repository by id: %d", repositoryID) + repo, _, err := client.Repositories.GetByID(ctx, repositoryID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get repository by ID: %d", repositoryID) + return "", err + } + + log.WithFields(f).Debugf("getting pull request by id: %d", pullRequestID) + pullRequest, _, err := client.PullRequests.Get(ctx, *repo.Owner.Login, *repo.Name, pullRequestID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get pull request by ID: %d", pullRequestID) + return "", err + } + + log.WithFields(f).Debugf("returning pull request html url: %s", *pullRequest.HTMLURL) + + return *pullRequest.HTMLURL, nil +} diff --git a/cla-backend-go/github/github_user.go b/cla-backend-go/github/github_user.go index 32a18d728..78745611e 100644 --- a/cla-backend-go/github/github_user.go +++ b/cla-backend-go/github/github_user.go @@ -9,7 +9,7 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/google/go-github/v33/github" + "github.com/google/go-github/v37/github" ) // GetUserDetails return github users details diff --git a/cla-backend-go/github/handlers.go b/cla-backend-go/github/handlers.go index 6c8217829..4f7516366 100644 --- a/cla-backend-go/github/handlers.go +++ b/cla-backend-go/github/handlers.go @@ -14,13 +14,13 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/user" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations" - gh "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/github" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations" + gh "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/github" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/gofrs/uuid" - ghLib "github.com/google/go-github/v33/github" + ghLib "github.com/google/go-github/v37/github" "github.com/savaki/dynastore" "golang.org/x/oauth2" "golang.org/x/oauth2/github" diff --git a/cla-backend-go/github/init.go b/cla-backend-go/github/init.go index 4dd5a5d0a..3e59e4c9a 100644 --- a/cla-backend-go/github/init.go +++ b/cla-backend-go/github/init.go @@ -7,7 +7,7 @@ var githubAppPrivateKey string var githubAppID int var secretAccessToken string -// Init initializes the required github variables +// Init initializes the required GitHub variables func Init(ghAppID int, ghAppPrivateKey string, secAccessToken string) { githubAppPrivateKey = ghAppPrivateKey githubAppID = ghAppID diff --git a/cla-backend-go/github/mock.go b/cla-backend-go/github/mock.go deleted file mode 100644 index 63a447c33..000000000 --- a/cla-backend-go/github/mock.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -// Code generated by MockGen. DO NOT EDIT. -// Source: protected_branch.go - -// Package github is a generated GoMock package. -package github - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - github "github.com/google/go-github/v33/github" -) - -// MockRepositories is a mock of Repositories interface -type MockRepositories struct { - ctrl *gomock.Controller - recorder *MockRepositoriesMockRecorder -} - -// MockRepositoriesMockRecorder is the mock recorder for MockRepositories -type MockRepositoriesMockRecorder struct { - mock *MockRepositories -} - -// NewMockRepositories creates a new mock instance -func NewMockRepositories(ctrl *gomock.Controller) *MockRepositories { - mock := &MockRepositories{ctrl: ctrl} - mock.recorder = &MockRepositoriesMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockRepositories) EXPECT() *MockRepositoriesMockRecorder { - return m.recorder -} - -// ListByOrg mocks base method -func (m *MockRepositories) ListByOrg(ctx context.Context, org string, opt *github.RepositoryListByOrgOptions) ([]*github.Repository, *github.Response, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListByOrg", ctx, org, opt) - ret0, _ := ret[0].([]*github.Repository) - ret1, _ := ret[1].(*github.Response) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// ListByOrg indicates an expected call of ListByOrg -func (mr *MockRepositoriesMockRecorder) ListByOrg(ctx, org, opt interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOrg", reflect.TypeOf((*MockRepositories)(nil).ListByOrg), ctx, org, opt) -} - -// Get mocks base method -func (m *MockRepositories) Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", ctx, owner, repo) - ret0, _ := ret[0].(*github.Repository) - ret1, _ := ret[1].(*github.Response) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// Get indicates an expected call of Get -func (mr *MockRepositoriesMockRecorder) Get(ctx, owner, repo interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRepositories)(nil).Get), ctx, owner, repo) -} - -// GetBranchProtection mocks base method -func (m *MockRepositories) GetBranchProtection(ctx context.Context, owner, repo, branch string) (*github.Protection, *github.Response, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBranchProtection", ctx, owner, repo, branch) - ret0, _ := ret[0].(*github.Protection) - ret1, _ := ret[1].(*github.Response) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBranchProtection indicates an expected call of GetBranchProtection -func (mr *MockRepositoriesMockRecorder) GetBranchProtection(ctx, owner, repo, branch interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBranchProtection", reflect.TypeOf((*MockRepositories)(nil).GetBranchProtection), ctx, owner, repo, branch) -} - -// UpdateBranchProtection mocks base method -func (m *MockRepositories) UpdateBranchProtection(ctx context.Context, owner, repo, branch string, preq *github.ProtectionRequest) (*github.Protection, *github.Response, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateBranchProtection", ctx, owner, repo, branch, preq) - ret0, _ := ret[0].(*github.Protection) - ret1, _ := ret[1].(*github.Response) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// UpdateBranchProtection indicates an expected call of UpdateBranchProtection -func (mr *MockRepositoriesMockRecorder) UpdateBranchProtection(ctx, owner, repo, branch, preq interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBranchProtection", reflect.TypeOf((*MockRepositories)(nil).UpdateBranchProtection), ctx, owner, repo, branch, preq) -} diff --git a/cla-backend-go/github/protected_branch.go b/cla-backend-go/github/protected_branch.go deleted file mode 100644 index 97d0fce65..000000000 --- a/cla-backend-go/github/protected_branch.go +++ /dev/null @@ -1,469 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package github - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/go-openapi/swag" - - "github.com/jinzhu/copier" - - log "github.com/communitybridge/easycla/cla-backend-go/logging" - - githubpkg "github.com/google/go-github/v33/github" - "go.uber.org/ratelimit" - "golang.org/x/time/rate" -) - -const ( - defaultBranchName = "master" -) - -var ( - // ErrBranchNotProtected indicates the situation where the branch is not enabled for protection on github side - ErrBranchNotProtected = errors.New("not protected") -) - -// rate limiting variables -var ( - // blockingRateLimit is useful for background tasks where the interaction is more predictable - blockingRateLimit = ratelimit.New(2) - // nonBlockingRateLimit is preferred when the github methods would be called realtime - // in this case we can call Allow method to check if can proceed or return error - nonBlockingRateLimit = rate.NewLimiter(2, 5) -) - -// Repositories is part of the interface working with github repositories, it's inside of the github client -// It's extracted here as interface so we can mock that functionality. -type Repositories interface { - ListByOrg(ctx context.Context, org string, opt *githubpkg.RepositoryListByOrgOptions) ([]*githubpkg.Repository, *githubpkg.Response, error) - Get(ctx context.Context, owner, repo string) (*githubpkg.Repository, *githubpkg.Response, error) - GetBranchProtection(ctx context.Context, owner, repo, branch string) (*githubpkg.Protection, *githubpkg.Response, error) - UpdateBranchProtection(ctx context.Context, owner, repo, branch string, preq *githubpkg.ProtectionRequest) (*githubpkg.Protection, *githubpkg.Response, error) -} - -type blockingRateLimitRepositories struct { - Repositories -} - -// NewBlockLimiterRepositories returns a new instance of Repositories interface with blocking rate limiting -// where when the limit is reached the next call blocks till the bucket is ready again -func NewBlockLimiterRepositories(repos Repositories) Repositories { - return blockingRateLimitRepositories{repos} -} - -func (b blockingRateLimitRepositories) ListByOrg(ctx context.Context, org string, opt *githubpkg.RepositoryListByOrgOptions) ([]*githubpkg.Repository, *githubpkg.Response, error) { - blockingRateLimit.Take() - return b.Repositories.ListByOrg(ctx, org, opt) -} - -func (b blockingRateLimitRepositories) Get(ctx context.Context, owner, repo string) (*githubpkg.Repository, *githubpkg.Response, error) { - blockingRateLimit.Take() - return b.Repositories.Get(ctx, owner, repo) -} - -func (b blockingRateLimitRepositories) GetBranchProtection(ctx context.Context, owner, repo, branch string) (*githubpkg.Protection, *githubpkg.Response, error) { - blockingRateLimit.Take() - return b.Repositories.GetBranchProtection(ctx, owner, repo, branch) -} - -func (b blockingRateLimitRepositories) UpdateBranchProtection(ctx context.Context, owner, repo, branch string, preq *githubpkg.ProtectionRequest) (*githubpkg.Protection, *githubpkg.Response, error) { - blockingRateLimit.Take() - return b.Repositories.UpdateBranchProtection(ctx, owner, repo, branch, preq) -} - -type nonBlockingRateLimitRepositories struct { - Repositories -} - -// NewNonBlockLimiterRepositories returns a new instance of Repositories interface with non blocking rate limiting -func NewNonBlockLimiterRepositories(repos Repositories) Repositories { - return nonBlockingRateLimitRepositories{repos} -} - -func (nb nonBlockingRateLimitRepositories) ListByOrg(ctx context.Context, org string, opt *githubpkg.RepositoryListByOrgOptions) ([]*githubpkg.Repository, *githubpkg.Response, error) { - if nonBlockingRateLimit.Allow() { - return nb.Repositories.ListByOrg(ctx, org, opt) - } - return nil, nil, fmt.Errorf("too many requests : %w", ErrRateLimited) -} - -func (nb nonBlockingRateLimitRepositories) Get(ctx context.Context, owner, repo string) (*githubpkg.Repository, *githubpkg.Response, error) { - if nonBlockingRateLimit.Allow() { - return nb.Repositories.Get(ctx, owner, repo) - } - return nil, nil, fmt.Errorf("too many requests : %w", ErrRateLimited) -} - -func (nb nonBlockingRateLimitRepositories) GetBranchProtection(ctx context.Context, owner, repo, branch string) (*githubpkg.Protection, *githubpkg.Response, error) { - if nonBlockingRateLimit.Allow() { - return nb.Repositories.GetBranchProtection(ctx, owner, repo, branch) - } - return nil, nil, fmt.Errorf("too many requests : %w", ErrRateLimited) -} - -func (nb nonBlockingRateLimitRepositories) UpdateBranchProtection(ctx context.Context, owner, repo, branch string, preq *githubpkg.ProtectionRequest) (*githubpkg.Protection, *githubpkg.Response, error) { - if nonBlockingRateLimit.Allow() { - return nb.Repositories.UpdateBranchProtection(ctx, owner, repo, branch, preq) - } - return nil, nil, fmt.Errorf("too many requests : %w", ErrRateLimited) -} - -type branchProtectionRepositoryConfig struct { - enableBlockingLimiter bool - enableNonBlockingLimiter bool -} - -// BranchProtectionRepositoryOption enables optional parameters to BranchProtectionRepository -type BranchProtectionRepositoryOption func(config *branchProtectionRepositoryConfig) - -// EnableBlockingLimiter enables the blocking limiter -func EnableBlockingLimiter() BranchProtectionRepositoryOption { - return func(config *branchProtectionRepositoryConfig) { - config.enableBlockingLimiter = true - } -} - -// EnableNonBlockingLimiter enables the non-blocking limiter -func EnableNonBlockingLimiter() BranchProtectionRepositoryOption { - return func(config *branchProtectionRepositoryConfig) { - config.enableNonBlockingLimiter = true - } -} - -// BranchProtectionRepository contains helper methods interacting with github api related to branch protection -type BranchProtectionRepository struct { - githubRepo Repositories -} - -// NewBranchProtectionRepository creates a new BranchProtectionRepository -func NewBranchProtectionRepository(githubRepo Repositories, opts ...BranchProtectionRepositoryOption) *BranchProtectionRepository { - config := &branchProtectionRepositoryConfig{} - for _, o := range opts { - o(config) - } - - if config.enableNonBlockingLimiter { - githubRepo = NewNonBlockLimiterRepositories(githubRepo) - } else if config.enableBlockingLimiter { - githubRepo = NewBlockLimiterRepositories(githubRepo) - } - - return &BranchProtectionRepository{ - githubRepo: githubRepo, - } -} - -// GetOwnerName retrieves the owner name of the given org and repo name -func (bp *BranchProtectionRepository) GetOwnerName(ctx context.Context, orgName, repoName string) (string, error) { - repoName = CleanGithubRepoName(repoName) - log.Debugf("GetOwnerName : getting owner name for org %s and repoName : %s", orgName, repoName) - listOpt := &githubpkg.RepositoryListByOrgOptions{ - ListOptions: githubpkg.ListOptions{ - PerPage: 30, - }, - } - for { - repos, resp, err := bp.githubRepo.ListByOrg(ctx, orgName, listOpt) - if err != nil { - if ok, wErr := checkAndWrapForKnownErrors(resp, err); ok { - return "", wErr - } - return "", err - } - - if len(repos) == 0 { - log.Warnf("GetOwnerName : no repos found under orgName : %s (maybe no access ?)", orgName) - return "", nil - } - - for _, repo := range repos { - if *repo.Name == repoName { - if repo.Owner != nil { - owner := *repo.Owner - return *owner.Login, nil - } - } - } - - // means we're at the end of it so exit - if resp.NextPage == 0 { - log.Warnf("GetOwnerName : owner name not found for org : %s and repo : %s", orgName, repoName) - return "", nil - } - - // set it to the next page - listOpt.Page = resp.NextPage - } -} - -// GetDefaultBranchForRepo helps with pulling the default branch for the given repo -func (bp *BranchProtectionRepository) GetDefaultBranchForRepo(ctx context.Context, owner, repoName string) (string, error) { - repoName = CleanGithubRepoName(repoName) - repo, resp, err := bp.githubRepo.Get(ctx, owner, repoName) - if err != nil { - if ok, wErr := checkAndWrapForKnownErrors(resp, err); ok { - return "", wErr - } - return "", err - } - - var defaultBranch string - if repo.DefaultBranch == nil { - defaultBranch = defaultBranchName - } else { - defaultBranch = *repo.DefaultBranch - } - - return defaultBranch, nil -} - -// GetProtectedBranch fetches the protected branch details -func (bp *BranchProtectionRepository) GetProtectedBranch(ctx context.Context, owner, repoName, protectedBranchName string) (*githubpkg.Protection, error) { - repoName = CleanGithubRepoName(repoName) - protection, resp, err := bp.githubRepo.GetBranchProtection(ctx, owner, repoName, protectedBranchName) - - if err != nil { - if ok, wErr := checkAndWrapForKnownErrors(resp, err); ok { - return nil, wErr - } - if resp != nil && resp.StatusCode == 404 { - if gErr, ok := err.(*githubpkg.ErrorResponse); ok { - if strings.Contains(strings.ToLower(gErr.Message), "not protected") { - return nil, ErrBranchNotProtected - } - } - } - - return nil, fmt.Errorf("fetching branch proteciton : %w", err) - } - return protection, err -} - -//EnableBranchProtection enables branch protection if not enabled and makes sure passed arguments such as enforceAdmin -//statusChecks are applied. The operation makes sure it doesn't override the existing checks. -func (bp *BranchProtectionRepository) EnableBranchProtection(ctx context.Context, owner, repoName, branchName string, enforceAdmin bool, enableStatusChecks, disableStatusChecks []string) error { - repoName = CleanGithubRepoName(repoName) - protectedBranch, err := bp.GetProtectedBranch(ctx, owner, repoName, branchName) - if err != nil && !errors.Is(err, ErrBranchNotProtected) { - return fmt.Errorf("fetching the protected branch for repo : %s : %w", repoName, err) - } - - branchProtectionRequest, err := createBranchProtectionRequest(protectedBranch, enableStatusChecks, disableStatusChecks, enforceAdmin) - if err != nil { - return fmt.Errorf("creating branch protection request failed : %v", err) - } - - _, resp, err := bp.githubRepo.UpdateBranchProtection(ctx, owner, repoName, branchName, branchProtectionRequest) - - if ok, wErr := checkAndWrapForKnownErrors(resp, err); ok { - return wErr - } - return err -} - -// createBranchProtectionRequest creates a branch protection request from existing protection -func createBranchProtectionRequest(protection *githubpkg.Protection, enableStatusChecks, disableStatusChecks []string, enforceAdmin bool) (*githubpkg.ProtectionRequest, error) { - var currentChecks *githubpkg.RequiredStatusChecks - if protection != nil { - currentChecks = protection.RequiredStatusChecks - } - requiredStatusChecks := mergeStatusChecks(currentChecks, enableStatusChecks, disableStatusChecks) - - branchProtectionRequest := &githubpkg.ProtectionRequest{ - RequiredStatusChecks: requiredStatusChecks, - EnforceAdmins: enforceAdmin, - } - - // don't have to check further in this case - if protection == nil { - return branchProtectionRequest, nil - } - - if protection.RequireLinearHistory != nil { - branchProtectionRequest.RequireLinearHistory = swag.Bool(protection.RequireLinearHistory.Enabled) - } - - if protection.AllowForcePushes != nil { - branchProtectionRequest.AllowForcePushes = swag.Bool(protection.AllowForcePushes.Enabled) - } - - if protection.AllowDeletions != nil { - branchProtectionRequest.AllowDeletions = swag.Bool(protection.AllowDeletions.Enabled) - } - - if protection.RequiredPullRequestReviews != nil { - var pullRequestReviewEnforcement githubpkg.PullRequestReviewsEnforcementRequest - if err := copier.Copy(&pullRequestReviewEnforcement, protection.RequiredPullRequestReviews); err != nil { - return nil, fmt.Errorf("copying from protected branch to request failed : requiredPullRequestReviews : %v", err) - } - - // github is not happy about null arrays, prefers empty arrays ... - //No subschema in "anyOf" matched. - //For 'properties/teams', nil is not an array. - //Not all subschemas of "allOf" matched. - var anyEnabled bool - if len(protection.RequiredPullRequestReviews.DismissalRestrictions.Users) > 0 { - anyEnabled = true - var users []string - for _, user := range protection.RequiredPullRequestReviews.DismissalRestrictions.Users { - users = append(users, *user.Login) - } - if pullRequestReviewEnforcement.DismissalRestrictionsRequest == nil { - pullRequestReviewEnforcement.DismissalRestrictionsRequest = &githubpkg.DismissalRestrictionsRequest{} - } - pullRequestReviewEnforcement.DismissalRestrictionsRequest.Users = &users - } - - if len(protection.RequiredPullRequestReviews.DismissalRestrictions.Teams) > 0 { - anyEnabled = true - var teams []string - for _, team := range protection.RequiredPullRequestReviews.DismissalRestrictions.Teams { - teams = append(teams, *team.Slug) - } - if pullRequestReviewEnforcement.DismissalRestrictionsRequest == nil { - pullRequestReviewEnforcement.DismissalRestrictionsRequest = &githubpkg.DismissalRestrictionsRequest{} - } - pullRequestReviewEnforcement.DismissalRestrictionsRequest.Teams = &teams - } - - if anyEnabled { - if pullRequestReviewEnforcement.DismissalRestrictionsRequest.Users == nil { - pullRequestReviewEnforcement.DismissalRestrictionsRequest.Users = &[]string{} - } - - if pullRequestReviewEnforcement.DismissalRestrictionsRequest.Teams == nil { - pullRequestReviewEnforcement.DismissalRestrictionsRequest.Teams = &[]string{} - } - - } - - branchProtectionRequest.RequiredPullRequestReviews = &pullRequestReviewEnforcement - } - - if protection.Restrictions != nil { - var restrictions githubpkg.BranchRestrictionsRequest - var anyEnabled bool - if len(protection.Restrictions.Users) > 0 { - anyEnabled = true - var users []string - for _, user := range protection.Restrictions.Users { - users = append(users, *user.Login) - } - restrictions.Users = users - } - - if len(protection.Restrictions.Teams) > 0 { - anyEnabled = true - var teams []string - for _, team := range protection.Restrictions.Teams { - teams = append(teams, *team.Slug) - } - restrictions.Teams = teams - } - - if len(protection.Restrictions.Apps) > 0 { - anyEnabled = true - var apps []string - for _, app := range protection.Restrictions.Apps { - apps = append(apps, *app.Slug) - } - restrictions.Apps = apps - } - - // make sure we don't send nil arrays ... - if anyEnabled { - if restrictions.Users == nil { - restrictions.Users = []string{} - } - - if restrictions.Teams == nil { - restrictions.Teams = []string{} - } - - if restrictions.Apps == nil { - restrictions.Apps = []string{} - } - } - - branchProtectionRequest.Restrictions = &restrictions - } - - return branchProtectionRequest, nil -} - -//mergeStatusChecks merges the current checks with the new ones and disable the ones that are specified -func mergeStatusChecks(currentCheck *githubpkg.RequiredStatusChecks, enableContexts, disableContexts []string) *githubpkg.RequiredStatusChecks { - - // seems github api is not happy with nils for arrays ;) - if len(enableContexts) == 0 { - enableContexts = []string{} - } - - if currentCheck == nil || len(currentCheck.Contexts) == 0 { - return &githubpkg.RequiredStatusChecks{ - Strict: true, - Contexts: enableContexts, - } - } - - finalContexts := []string{} - uniqueEnableContexts := map[string]bool{} - - for _, c := range currentCheck.Contexts { - // first disable the ones we're not interested into - found := false - if len(disableContexts) > 0 { - for _, disableContext := range disableContexts { - if disableContext == c { - found = true - break - } - } - } - - if found { - continue - } - - uniqueEnableContexts[c] = true - finalContexts = append(finalContexts, c) - } - - for _, c := range enableContexts { - if uniqueEnableContexts[c] { - continue - } - - uniqueEnableContexts[c] = true - finalContexts = append(finalContexts, c) - } - - currentCheck.Contexts = finalContexts - currentCheck.Strict = true - - return currentCheck -} - -//IsEnforceAdminEnabled checks if enforce admin option is enabled for the branch protection -func IsEnforceAdminEnabled(protection *githubpkg.Protection) bool { - if protection.EnforceAdmins == nil { - return false - } - - return protection.EnforceAdmins.Enabled -} - -// CleanGithubRepoName removes the orgname if existing in the string -func CleanGithubRepoName(githubRepoName string) string { - if strings.Contains(githubRepoName, "/") { - parts := strings.Split(githubRepoName, "/") - githubRepoName = parts[len(parts)-1] - } - return githubRepoName -} diff --git a/cla-backend-go/github/protected_branch_test.go b/cla-backend-go/github/protected_branch_test.go deleted file mode 100644 index 2193f2b98..000000000 --- a/cla-backend-go/github/protected_branch_test.go +++ /dev/null @@ -1,351 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package github - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/go-openapi/swag" - - "github.com/golang/mock/gomock" - - "github.com/bmizerany/assert" - githubsdk "github.com/google/go-github/v33/github" -) - -// TestMergeStatusChecks tests the functionality of where we enable/disable checks -func TestMergeStatusChecks(t *testing.T) { - - testCases := []struct { - Name string - currentChecks *githubsdk.RequiredStatusChecks - expectedChecks *githubsdk.RequiredStatusChecks - enableContexts []string - disableContexts []string - }{ - { - Name: "all empty", - expectedChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{}, - }, - }, - { - Name: "empty state enable", - expectedChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"EasyCLA"}, - }, - enableContexts: []string{"EasyCLA"}, - }, - { - Name: "preserve existing enable more", - currentChecks: &githubsdk.RequiredStatusChecks{ - Contexts: []string{"travis-ci"}, - }, - expectedChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"travis-ci", "EasyCLA"}, - }, - enableContexts: []string{"EasyCLA"}, - }, - { - Name: "preserve existing disable some", - currentChecks: &githubsdk.RequiredStatusChecks{ - Contexts: []string{"travis-ci", "EasyCLA"}, - }, - expectedChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"travis-ci"}, - }, - disableContexts: []string{"EasyCLA"}, - }, - { - Name: "add and remove in same operation", - currentChecks: &githubsdk.RequiredStatusChecks{ - Contexts: []string{"travis-ci", "DCO", "EasyCLA"}, - }, - expectedChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"travis-ci", "EasyCLA", "CodeQL"}, - }, - enableContexts: []string{"CodeQL"}, - disableContexts: []string{"DCO"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(tt *testing.T) { - result := mergeStatusChecks(tc.currentChecks, tc.enableContexts, tc.disableContexts) - assert.Equal(tt, tc.expectedChecks, result) - }) - } -} - -func TestEnableBranchProtection(t *testing.T) { - owner := "johnenable" - repo := "johnsrepoenable" - branchName := defaultBranchName - - testCases := []struct { - Name string - Checks []string - Protection *githubsdk.Protection - ProtectionRequest *githubsdk.ProtectionRequest - Err error - }{ - { - Name: "success", - Checks: []string{"easyCLA"}, - Protection: &githubsdk.Protection{}, - ProtectionRequest: &githubsdk.ProtectionRequest{ - EnforceAdmins: true, - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"easyCLA"}, - }, - }, - }, - { - Name: "preserve existing checks", - Checks: []string{"easyCLA"}, - Protection: &githubsdk.Protection{ - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"circle/ci"}, - }, - }, - ProtectionRequest: &githubsdk.ProtectionRequest{ - EnforceAdmins: true, - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"circle/ci", "easyCLA"}, - }, - }, - }, - { - Name: "preserve existing settings", - Checks: []string{"easyCLA"}, - Protection: &githubsdk.Protection{ - RequiredPullRequestReviews: &githubsdk.PullRequestReviewsEnforcement{ - RequireCodeOwnerReviews: true, - RequiredApprovingReviewCount: 2, - DismissalRestrictions: &githubsdk.DismissalRestrictions{ - Users: []*githubsdk.User{ - {Login: swag.String("alex")}, - }, - Teams: []*githubsdk.Team{ - {Slug: swag.String("alpha")}, - }, - }, - }, - Restrictions: &githubsdk.BranchRestrictions{ - Users: []*githubsdk.User{ - {Login: swag.String("john")}, - }, - Teams: []*githubsdk.Team{ - {Slug: swag.String("easyCLA-Team")}, - }, - }, - RequireLinearHistory: &githubsdk.RequireLinearHistory{Enabled: true}, - AllowForcePushes: &githubsdk.AllowForcePushes{Enabled: true}, - AllowDeletions: &githubsdk.AllowDeletions{Enabled: true}, - }, - ProtectionRequest: &githubsdk.ProtectionRequest{ - RequiredPullRequestReviews: &githubsdk.PullRequestReviewsEnforcementRequest{ - RequireCodeOwnerReviews: true, - RequiredApprovingReviewCount: 2, - DismissalRestrictionsRequest: &githubsdk.DismissalRestrictionsRequest{ - Users: &[]string{"alex"}, - Teams: &[]string{"alpha"}, - }, - }, - Restrictions: &githubsdk.BranchRestrictionsRequest{ - Users: []string{"john"}, - Teams: []string{"easyCLA-Team"}, - Apps: []string{}, - }, - EnforceAdmins: true, - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: true, - Contexts: []string{"easyCLA"}, - }, - RequireLinearHistory: swag.Bool(true), - AllowForcePushes: swag.Bool(true), - AllowDeletions: swag.Bool(true), - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(tt *testing.T) { - ctrl := gomock.NewController(tt) - // Assert that Bar() is invoked. - defer ctrl.Finish() - - m := NewMockRepositories(ctrl) - m. - EXPECT(). - GetBranchProtection(gomock.Any(), owner, repo, branchName). - Return(tc.Protection, nil, nil) - m. - EXPECT(). - UpdateBranchProtection(gomock.Any(), owner, repo, branchName, tc.ProtectionRequest). - Return(nil, nil, nil) - - branchProtectionRepo := NewBranchProtectionRepository(m) - err := branchProtectionRepo.EnableBranchProtection(context.Background(), owner, repo, branchName, true, tc.Checks, nil) - if err != nil { - tt.Errorf("enable branch proteciton failed : %v", err) - } - }) - } - -} - -func TestNonBlockingRateLimitRepositories_GetBranchProtection(t *testing.T) { - owner := "johnblocking" - repo := "johnsrepoblocking" - branchName := defaultBranchName - - t.Run("no limit reached", func(tt *testing.T) { - ctrl := gomock.NewController(tt) - defer ctrl.Finish() - - protection := &githubsdk.Protection{ - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"circle/ci"}, - }, - } - - m := NewMockRepositories(ctrl) - m. - EXPECT(). - GetBranchProtection(gomock.Any(), owner, repo, branchName). - Return(protection, nil, nil) - - nonBlockLimitRepo := NewBranchProtectionRepository(m, EnableNonBlockingLimiter()) - p, err := nonBlockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) - if err != nil { - tt.Errorf("no error expected : %v", err) - } - assert.Equal(tt, protection, p) - }) - - t.Run("limit reached", func(tt *testing.T) { - ctrl := gomock.NewController(tt) - defer ctrl.Finish() - - protection := &githubsdk.Protection{ - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"circle/ci"}, - }, - } - - m := NewMockRepositories(ctrl) - m. - EXPECT(). - GetBranchProtection(gomock.Any(), owner, repo, branchName). - Return(protection, nil, nil).AnyTimes() - - nonBlockLimitRepo := NewBranchProtectionRepository(m, EnableNonBlockingLimiter()) - // call it 100 times in loop to make it fail - var expectedErr error - for i := 0; i < 100; i++ { - _, err := nonBlockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) - if err != nil { - expectedErr = err - break - } - } - - if expectedErr == nil { - tt.Fatalf("no error returned") - return - } - - if !errors.Is(expectedErr, ErrRateLimited) { - tt.Fatalf("was expecting ErrRateLimited got : %v", expectedErr) - return - } - }) -} - -func TestBlockingRateLimitRepositories_GetBranchProtection(t *testing.T) { - owner := "john" - repo := "johnsrepo" - branchName := defaultBranchName - - t.Run("no limit reached", func(tt *testing.T) { - ctrl := gomock.NewController(tt) - defer ctrl.Finish() - - protection := &githubsdk.Protection{ - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"circle/ci"}, - }, - } - - m := NewMockRepositories(ctrl) - m. - EXPECT(). - GetBranchProtection(gomock.Any(), owner, repo, branchName). - Return(protection, nil, nil) - - blockLimitRepo := NewBranchProtectionRepository(m, EnableBlockingLimiter()) - p, err := blockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) - if err != nil { - tt.Errorf("no error expected : %v", err) - } - assert.Equal(tt, protection, p) - }) - - t.Run("limit reached", func(tt *testing.T) { - ctrl := gomock.NewController(tt) - defer ctrl.Finish() - - protection := &githubsdk.Protection{ - RequiredStatusChecks: &githubsdk.RequiredStatusChecks{ - Strict: false, - Contexts: []string{"circle/ci"}, - }, - } - - m := NewMockRepositories(ctrl) - m. - EXPECT(). - GetBranchProtection(gomock.Any(), owner, repo, branchName). - Return(protection, nil, nil).AnyTimes() - - blockLimitRepo := NewBranchProtectionRepository(m, EnableBlockingLimiter()) - - // call it 100 times in loop to make it fail - var expectedErr error - start := time.Now() - for i := 0; i < 10; i++ { - _, err := blockLimitRepo.GetProtectedBranch(context.Background(), owner, repo, branchName) - if err != nil { - expectedErr = err - break - } - } - elapsed := time.Since(start) - - if expectedErr != nil { - tt.Fatalf("no error was expected got : %v", expectedErr) - return - } - - if elapsed < 4*time.Second { - tt.Fatalf("is rate limit enabled") - } - - }) -} diff --git a/cla-backend-go/github_organizations/handlers.go b/cla-backend-go/github_organizations/handlers.go index 1e0e87bcb..721113f1a 100644 --- a/cla-backend-go/github_organizations/handlers.go +++ b/cla-backend-go/github_organizations/handlers.go @@ -11,9 +11,9 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "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/github_organizations" + "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/github_organizations" "github.com/communitybridge/easycla/cla-backend-go/github" "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/communitybridge/easycla/cla-backend-go/utils" @@ -22,13 +22,13 @@ import ( ) // Configure setups handlers on api with service -func Configure(api *operations.ClaAPI, service Service, eventService events.Service) { +func Configure(api *operations.ClaAPI, service ServiceInterface, eventService events.Service) { api.GithubOrganizationsGetProjectGithubOrganizationsHandler = github_organizations.GetProjectGithubOrganizationsHandlerFunc( func(params github_organizations.GetProjectGithubOrganizationsParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - result, err := service.GetGithubOrganizations(ctx, params.ProjectSFID) + result, err := service.GetGitHubOrganizations(ctx, params.ProjectSFID) if err != nil { if _, ok := err.(*v2ProjectServiceClient.GetProjectNotFound); ok { return github_organizations.NewGetProjectGithubOrganizationsNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ @@ -53,7 +53,7 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv }) } - if !utils.ValidateAutoEnabledClaGroupID(params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + 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", @@ -65,7 +65,7 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv return github_organizations.NewAddProjectGithubOrganizationNotFound().WithPayload(errorResponse(err)) } - result, err := service.AddGithubOrganization(ctx, params.ProjectSFID, params.Body) + result, err := service.AddGitHubOrganization(ctx, params.ProjectSFID, params.Body) if err != nil { if _, ok := err.(*v2ProjectServiceClient.GetProjectNotFound); ok { return github_organizations.NewAddProjectGithubOrganizationNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ @@ -89,11 +89,11 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv if params.Body.BranchProtectionEnabled != nil { branchProtectionEnabled = *params.Body.BranchProtectionEnabled } - eventService.LogEvent(&events.LogEventArgs{ - UserID: claUser.UserID, - EventType: events.GitHubOrganizationAdded, - ExternalProjectID: params.ProjectSFID, - LfUsername: claUser.LFUsername, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + UserID: claUser.UserID, + EventType: events.GitHubOrganizationAdded, + ProjectSFID: params.ProjectSFID, + LfUsername: claUser.LFUsername, EventData: &events.GitHubOrganizationAddedEventData{ GitHubOrganizationName: *params.Body.OrganizationName, AutoEnabled: autoEnabled, @@ -114,7 +114,7 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv return github_organizations.NewDeleteProjectGithubOrganizationNotFound().WithPayload(errorResponse(err)) } - err = service.DeleteGithubOrganization(ctx, params.ProjectSFID, params.OrgName) + err = service.DeleteGitHubOrganization(ctx, params.ProjectSFID, params.OrgName) if err != nil { if _, ok := err.(*v2ProjectServiceClient.GetProjectNotFound); ok { return github_organizations.NewDeleteProjectGithubOrganizationNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ @@ -125,11 +125,11 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv return github_organizations.NewDeleteProjectGithubOrganizationBadRequest().WithPayload(errorResponse(err)) } - eventService.LogEvent(&events.LogEventArgs{ - UserID: claUser.UserID, - EventType: events.GitHubOrganizationDeleted, - ExternalProjectID: params.ProjectSFID, - LfUsername: claUser.LFUsername, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + UserID: claUser.UserID, + EventType: events.GitHubOrganizationDeleted, + ProjectSFID: params.ProjectSFID, + LfUsername: claUser.LFUsername, EventData: &events.GitHubOrganizationDeletedEventData{ GitHubOrganizationName: params.OrgName, }, @@ -148,14 +148,14 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv }) } - if !utils.ValidateAutoEnabledClaGroupID(params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + if !utils.ValidateAutoEnabledClaGroupID(*params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(&models.ErrorResponse{ Code: "400", Message: "EasyCLA - 400 Bad Request - AutoEnabledClaGroupID can't be empty when AutoEnabled", }) } - err := service.UpdateGithubOrganization(ctx, params.ProjectSFID, params.OrgName, *params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID, params.Body.BranchProtectionEnabled) + err := service.UpdateGitHubOrganization(ctx, params.ProjectSFID, params.OrgName, *params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID, params.Body.BranchProtectionEnabled) if err != nil { if errors.Is(err, projects_cla_groups.ErrCLAGroupDoesNotExist) { return github_organizations.NewUpdateProjectGithubOrganizationConfigNotFound().WithPayload(errorResponse(err)) @@ -163,11 +163,11 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv return github_organizations.NewUpdateProjectGithubOrganizationConfigBadRequest().WithPayload(errorResponse(err)) } - eventService.LogEvent(&events.LogEventArgs{ - UserID: claUser.UserID, - EventType: events.GitHubOrganizationUpdated, - ExternalProjectID: params.ProjectSFID, - LfUsername: claUser.LFUsername, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + UserID: claUser.UserID, + EventType: events.GitHubOrganizationUpdated, + ProjectSFID: params.ProjectSFID, + LfUsername: claUser.LFUsername, EventData: &events.GitHubOrganizationUpdatedEventData{ GitHubOrganizationName: params.OrgName, AutoEnabled: *params.Body.AutoEnabled, @@ -176,6 +176,7 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv return github_organizations.NewUpdateProjectGithubOrganizationConfigOK() }) + } type codedResponse interface { diff --git a/cla-backend-go/github_organizations/helpers.go b/cla-backend-go/github_organizations/helpers.go index bd34f67b5..c1610aa53 100644 --- a/cla-backend-go/github_organizations/helpers.go +++ b/cla-backend-go/github_organizations/helpers.go @@ -5,9 +5,11 @@ package github_organizations import ( "context" + "fmt" + netURL "net/url" "sync" - "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" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" @@ -32,16 +34,23 @@ func buildGithubOrganizationListModels(ctx context.Context, githubOrganizations go func(ghorg *models.GithubOrganization) { defer wg.Done() ghorg.GithubInfo = &models.GithubOrganizationGithubInfo{} - log.WithFields(f).Debugf("Loading GitHub organization details: %s...", ghorg.OrganizationName) + log.WithFields(f).Debugf("loading GitHub organization details: %s...", ghorg.OrganizationName) user, err := github.GetUserDetails(ghorg.OrganizationName) if err != nil { ghorg.GithubInfo.Error = err.Error() } else { url := strfmt.URI(*user.HTMLURL) + installURL := netURL.URL{ + Scheme: "https", + Host: "github.com", + Path: fmt.Sprintf("/organizations/%s/settings/installations/%d", ghorg.OrganizationName, ghorg.OrganizationInstallationID), + } + installationURL := strfmt.URI(installURL.String()) ghorg.GithubInfo.Details = &models.GithubOrganizationGithubInfoDetails{ - Bio: user.Bio, - HTMLURL: &url, - ID: user.ID, + Bio: user.Bio, + HTMLURL: &url, + ID: user.ID, + InstallationURL: &installationURL, } } @@ -50,15 +59,15 @@ func buildGithubOrganizationListModels(ctx context.Context, githubOrganizations } if ghorg.OrganizationInstallationID != 0 { - log.WithFields(f).Debugf("Loading GitHub repository list based on installation id: %d...", ghorg.OrganizationInstallationID) + log.WithFields(f).Debugf("loading GitHub repository list directly from GitHub based on the installation id: %d...", ghorg.OrganizationInstallationID) list, err := github.GetInstallationRepositories(ctx, ghorg.OrganizationInstallationID) if err != nil { - log.WithFields(f).Warnf("unable to get repositories for installation id : %d", ghorg.OrganizationInstallationID) + log.WithFields(f).Warnf("unable to get repositories from GitHub for the installation id: %d", ghorg.OrganizationInstallationID) ghorg.Repositories.Error = err.Error() return } - log.WithFields(f).Debugf("Found %d GitHub repositories using installation id: %d...", + log.WithFields(f).Debugf("found %d repositories from GitHub using the installation id: %d...", len(list), ghorg.OrganizationInstallationID) for _, repoInfo := range list { ghorg.Repositories.List = append(ghorg.Repositories.List, &models.GithubRepositoryInfo{ diff --git a/cla-backend-go/github_organizations/mock.go b/cla-backend-go/github_organizations/mock.go deleted file mode 100644 index e446e0434..000000000 --- a/cla-backend-go/github_organizations/mock.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -// Code generated by MockGen. DO NOT EDIT. -// Source: repository.go - -// Package github_organizations is a generated GoMock package. -package github_organizations - -import ( - context "context" - reflect "reflect" - - models "github.com/communitybridge/easycla/cla-backend-go/gen/models" - gomock "github.com/golang/mock/gomock" -) - -// MockRepository is a mock of Repository interface -type MockRepository struct { - ctrl *gomock.Controller - recorder *MockRepositoryMockRecorder -} - -// MockRepositoryMockRecorder is the mock recorder for MockRepository -type MockRepositoryMockRecorder struct { - mock *MockRepository -} - -// NewMockRepository creates a new mock instance -func NewMockRepository(ctrl *gomock.Controller) *MockRepository { - mock := &MockRepository{ctrl: ctrl} - mock.recorder = &MockRepositoryMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { - return m.recorder -} - -// AddGithubOrganization mocks base method -func (m *MockRepository) AddGithubOrganization(ctx context.Context, parentProjectSFID, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddGithubOrganization", ctx, parentProjectSFID, projectSFID, input) - ret0, _ := ret[0].(*models.GithubOrganization) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddGithubOrganization indicates an expected call of AddGithubOrganization -func (mr *MockRepositoryMockRecorder) AddGithubOrganization(ctx, parentProjectSFID, projectSFID, input interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGithubOrganization", reflect.TypeOf((*MockRepository)(nil).AddGithubOrganization), ctx, parentProjectSFID, projectSFID, input) -} - -// GetGithubOrganizations mocks base method -func (m *MockRepository) GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGithubOrganizations", ctx, projectSFID) - ret0, _ := ret[0].(*models.GithubOrganizations) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetGithubOrganizations indicates an expected call of GetGithubOrganizations -func (mr *MockRepositoryMockRecorder) GetGithubOrganizations(ctx, projectSFID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganizations", reflect.TypeOf((*MockRepository)(nil).GetGithubOrganizations), ctx, projectSFID) -} - -// GetGithubOrganizationsByParent mocks base method -func (m *MockRepository) GetGithubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGithubOrganizationsByParent", ctx, parentProjectSFID) - ret0, _ := ret[0].(*models.GithubOrganizations) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetGithubOrganizationsByParent indicates an expected call of GetGithubOrganizationsByParent -func (mr *MockRepositoryMockRecorder) GetGithubOrganizationsByParent(ctx, parentProjectSFID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganizationsByParent", reflect.TypeOf((*MockRepository)(nil).GetGithubOrganizationsByParent), ctx, parentProjectSFID) -} - -// GetGithubOrganization mocks base method -func (m *MockRepository) GetGithubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGithubOrganization", ctx, githubOrganizationName) - ret0, _ := ret[0].(*models.GithubOrganization) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetGithubOrganization indicates an expected call of GetGithubOrganization -func (mr *MockRepositoryMockRecorder) GetGithubOrganization(ctx, githubOrganizationName interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganization", reflect.TypeOf((*MockRepository)(nil).GetGithubOrganization), ctx, githubOrganizationName) -} - -// GetGithubOrganizationByName mocks base method -func (m *MockRepository) GetGithubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGithubOrganizationByName", ctx, githubOrganizationName) - ret0, _ := ret[0].(*models.GithubOrganizations) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetGithubOrganizationByName indicates an expected call of GetGithubOrganizationByName -func (mr *MockRepositoryMockRecorder) GetGithubOrganizationByName(ctx, githubOrganizationName interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganizationByName", reflect.TypeOf((*MockRepository)(nil).GetGithubOrganizationByName), ctx, githubOrganizationName) -} - -// UpdateGithubOrganization mocks base method -func (m *MockRepository) UpdateGithubOrganization(ctx context.Context, projectSFID, organizationName string, autoEnabled, branchProtectionEnabled bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateGithubOrganization", ctx, projectSFID, organizationName, autoEnabled, branchProtectionEnabled) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateGithubOrganization indicates an expected call of UpdateGithubOrganization -func (mr *MockRepositoryMockRecorder) UpdateGithubOrganization(ctx, projectSFID, organizationName, autoEnabled, branchProtectionEnabled interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGithubOrganization", reflect.TypeOf((*MockRepository)(nil).UpdateGithubOrganization), ctx, projectSFID, organizationName, autoEnabled, branchProtectionEnabled) -} - -// DeleteGithubOrganization mocks base method -func (m *MockRepository) DeleteGithubOrganization(ctx context.Context, projectSFID, githubOrgName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteGithubOrganization", ctx, projectSFID, githubOrgName) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteGithubOrganization indicates an expected call of DeleteGithubOrganization -func (mr *MockRepositoryMockRecorder) DeleteGithubOrganization(ctx, projectSFID, githubOrgName interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGithubOrganization", reflect.TypeOf((*MockRepository)(nil).DeleteGithubOrganization), ctx, projectSFID, githubOrgName) -} - -// DeleteGithubOrganizationByParent mocks base method -func (m *MockRepository) DeleteGithubOrganizationByParent(ctx context.Context, parentProjectSFID, githubOrgName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteGithubOrganizationByParent", ctx, parentProjectSFID, githubOrgName) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteGithubOrganizationByParent indicates an expected call of DeleteGithubOrganizationByParent -func (mr *MockRepositoryMockRecorder) DeleteGithubOrganizationByParent(ctx, parentProjectSFID, githubOrgName interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGithubOrganizationByParent", reflect.TypeOf((*MockRepository)(nil).DeleteGithubOrganizationByParent), ctx, parentProjectSFID, githubOrgName) -} diff --git a/cla-backend-go/github_organizations/mock/mock_repository.go b/cla-backend-go/github_organizations/mock/mock_repository.go new file mode 100644 index 000000000..8e86ee1e4 --- /dev/null +++ b/cla-backend-go/github_organizations/mock/mock_repository.go @@ -0,0 +1,157 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github_organizations/repository.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + gomock "github.com/golang/mock/gomock" +) + +// MockRepositoryInterface is a mock of RepositoryInterface interface. +type MockRepositoryInterface struct { + ctrl *gomock.Controller + recorder *MockRepositoryInterfaceMockRecorder +} + +// MockRepositoryInterfaceMockRecorder is the mock recorder for MockRepositoryInterface. +type MockRepositoryInterfaceMockRecorder struct { + mock *MockRepositoryInterface +} + +// NewMockRepositoryInterface creates a new mock instance. +func NewMockRepositoryInterface(ctrl *gomock.Controller) *MockRepositoryInterface { + mock := &MockRepositoryInterface{ctrl: ctrl} + mock.recorder = &MockRepositoryInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryInterface) EXPECT() *MockRepositoryInterfaceMockRecorder { + return m.recorder +} + +// AddGitHubOrganization mocks base method. +func (m *MockRepositoryInterface) AddGitHubOrganization(ctx context.Context, parentProjectSFID, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddGitHubOrganization", ctx, parentProjectSFID, projectSFID, input) + ret0, _ := ret[0].(*models.GithubOrganization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddGitHubOrganization indicates an expected call of AddGitHubOrganization. +func (mr *MockRepositoryInterfaceMockRecorder) AddGitHubOrganization(ctx, parentProjectSFID, projectSFID, input interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGitHubOrganization", reflect.TypeOf((*MockRepositoryInterface)(nil).AddGitHubOrganization), ctx, parentProjectSFID, projectSFID, input) +} + +// DeleteGitHubOrganization mocks base method. +func (m *MockRepositoryInterface) DeleteGitHubOrganization(ctx context.Context, projectSFID, githubOrgName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteGitHubOrganization", ctx, projectSFID, githubOrgName) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteGitHubOrganization indicates an expected call of DeleteGitHubOrganization. +func (mr *MockRepositoryInterfaceMockRecorder) DeleteGitHubOrganization(ctx, projectSFID, githubOrgName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGitHubOrganization", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteGitHubOrganization), ctx, projectSFID, githubOrgName) +} + +// DeleteGitHubOrganizationByParent mocks base method. +func (m *MockRepositoryInterface) DeleteGitHubOrganizationByParent(ctx context.Context, parentProjectSFID, githubOrgName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteGitHubOrganizationByParent", ctx, parentProjectSFID, githubOrgName) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteGitHubOrganizationByParent indicates an expected call of DeleteGitHubOrganizationByParent. +func (mr *MockRepositoryInterfaceMockRecorder) DeleteGitHubOrganizationByParent(ctx, parentProjectSFID, githubOrgName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGitHubOrganizationByParent", reflect.TypeOf((*MockRepositoryInterface)(nil).DeleteGitHubOrganizationByParent), ctx, parentProjectSFID, githubOrgName) +} + +// GetGitHubOrganization mocks base method. +func (m *MockRepositoryInterface) GetGitHubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGitHubOrganization", ctx, githubOrganizationName) + ret0, _ := ret[0].(*models.GithubOrganization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGitHubOrganization indicates an expected call of GetGitHubOrganization. +func (mr *MockRepositoryInterfaceMockRecorder) GetGitHubOrganization(ctx, githubOrganizationName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganization", reflect.TypeOf((*MockRepositoryInterface)(nil).GetGitHubOrganization), ctx, githubOrganizationName) +} + +// GetGitHubOrganizationByName mocks base method. +func (m *MockRepositoryInterface) GetGitHubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGitHubOrganizationByName", ctx, githubOrganizationName) + ret0, _ := ret[0].(*models.GithubOrganizations) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGitHubOrganizationByName indicates an expected call of GetGitHubOrganizationByName. +func (mr *MockRepositoryInterfaceMockRecorder) GetGitHubOrganizationByName(ctx, githubOrganizationName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganizationByName", reflect.TypeOf((*MockRepositoryInterface)(nil).GetGitHubOrganizationByName), ctx, githubOrganizationName) +} + +// GetGitHubOrganizations mocks base method. +func (m *MockRepositoryInterface) GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGitHubOrganizations", ctx, projectSFID) + ret0, _ := ret[0].(*models.GithubOrganizations) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGitHubOrganizations indicates an expected call of GetGitHubOrganizations. +func (mr *MockRepositoryInterfaceMockRecorder) GetGitHubOrganizations(ctx, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganizations", reflect.TypeOf((*MockRepositoryInterface)(nil).GetGitHubOrganizations), ctx, projectSFID) +} + +// GetGitHubOrganizationsByParent mocks base method. +func (m *MockRepositoryInterface) GetGitHubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGitHubOrganizationsByParent", ctx, parentProjectSFID) + ret0, _ := ret[0].(*models.GithubOrganizations) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGitHubOrganizationsByParent indicates an expected call of GetGitHubOrganizationsByParent. +func (mr *MockRepositoryInterfaceMockRecorder) GetGitHubOrganizationsByParent(ctx, parentProjectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganizationsByParent", reflect.TypeOf((*MockRepositoryInterface)(nil).GetGitHubOrganizationsByParent), ctx, parentProjectSFID) +} + +// UpdateGitHubOrganization mocks base method. +func (m *MockRepositoryInterface) UpdateGitHubOrganization(ctx context.Context, projectSFID, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool, enabled *bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateGitHubOrganization", ctx, projectSFID, organizationName, autoEnabled, autoEnabledClaGroupID, branchProtectionEnabled, enabled) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateGitHubOrganization indicates an expected call of UpdateGitHubOrganization. +func (mr *MockRepositoryInterfaceMockRecorder) UpdateGitHubOrganization(ctx, projectSFID, organizationName, autoEnabled, autoEnabledClaGroupID, branchProtectionEnabled, enabled interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGitHubOrganization", reflect.TypeOf((*MockRepositoryInterface)(nil).UpdateGitHubOrganization), ctx, projectSFID, organizationName, autoEnabled, autoEnabledClaGroupID, branchProtectionEnabled, enabled) +} diff --git a/cla-backend-go/github_organizations/models.go b/cla-backend-go/github_organizations/models.go index b63ce0958..db3755115 100644 --- a/cla-backend-go/github_organizations/models.go +++ b/cla-backend-go/github_organizations/models.go @@ -3,7 +3,7 @@ package github_organizations -import "github.com/communitybridge/easycla/cla-backend-go/gen/models" +import "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" // GithubOrganization is data model for github organizations type GithubOrganization struct { @@ -14,6 +14,7 @@ type GithubOrganization struct { OrganizationNameLower string `json:"organization_name_lower,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"` @@ -29,6 +30,7 @@ func ToModel(in *GithubOrganization) *models.GithubOrganization { OrganizationName: in.OrganizationName, OrganizationSfid: in.OrganizationSFID, Version: in.Version, + Enabled: in.Enabled, AutoEnabled: in.AutoEnabled, AutoEnabledClaGroupID: in.AutoEnabledClaGroupID, BranchProtectionEnabled: in.BranchProtectionEnabled, diff --git a/cla-backend-go/github_organizations/repository.go b/cla-backend-go/github_organizations/repository.go index 6340e69c3..99596fa3a 100644 --- a/cla-backend-go/github_organizations/repository.go +++ b/cla-backend-go/github_organizations/repository.go @@ -19,7 +19,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" ) @@ -30,42 +30,43 @@ const ( ProjectSFIDOrganizationNameIndex = "project-sfid-organization-name-index" ) -// errors var ( + // ErrOrganizationDoesNotExist organization does not exist error ErrOrganizationDoesNotExist = errors.New("github organization does not exist in cla") ) -// Repository interface defines the functions for the github organizations data model -type Repository interface { - AddGithubOrganization(ctx context.Context, parentProjectSFID string, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) - GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) - GetGithubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) - GetGithubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) - GetGithubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) - UpdateGithubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error - DeleteGithubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error - DeleteGithubOrganizationByParent(ctx context.Context, parentProjectSFID string, githubOrgName string) error +// RepositoryInterface interface defines the functions for the github organizations data model +type RepositoryInterface interface { + AddGitHubOrganization(ctx context.Context, parentProjectSFID string, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) + GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) + GetGitHubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) + GetGitHubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) + GetGitHubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) + UpdateGitHubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool, enabled *bool) error + DeleteGitHubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error + DeleteGitHubOrganizationByParent(ctx context.Context, parentProjectSFID string, githubOrgName string) error } -type repository struct { +// Repository object/struct +type Repository struct { stage string dynamoDBClient *dynamodb.DynamoDB githubOrgTableName string } // NewRepository creates a new instance of the githubOrganizations repository -func NewRepository(awsSession *session.Session, stage string) repository { - return repository{ +func NewRepository(awsSession *session.Session, stage string) Repository { + return Repository{ stage: stage, dynamoDBClient: dynamodb.New(awsSession), githubOrgTableName: fmt.Sprintf("cla-%s-github-orgs", stage), } } -// AddGithubOrganization add github organization logic -func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectSFID string, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) { +// AddGitHubOrganization add github organization logic +func (repo Repository) AddGitHubOrganization(ctx context.Context, parentProjectSFID string, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) { f := logrus.Fields{ - "functionName": "AddGitHubOrganization", + "functionName": "v1.github_organizations.repository.AddGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "parentProjectSFID": parentProjectSFID, "projectSFID": projectSFID, @@ -75,7 +76,7 @@ func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectS } // First, let's check to see if we have an existing github organization with the same name - existingRecord, getErr := repo.GetGithubOrganizationByName(ctx, utils.StringValue(input.OrganizationName)) + existingRecord, getErr := repo.GetGitHubOrganizationByName(ctx, utils.StringValue(input.OrganizationName)) if getErr != nil { log.WithFields(f).WithError(getErr).Debug("unable to locate existing github organization by name") } @@ -108,12 +109,15 @@ func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectS } // Attempt to simply update the existing record - we should only have one - updateErr := repo.UpdateGithubOrganization(ctx, + // activate GH org by updating the enabled flag + enabled := true + updateErr := repo.UpdateGitHubOrganization(ctx, projectSFID, utils.StringValue(input.OrganizationName), autoEnabled, autoEnabledCLAGroupID, branchProtectionEnabled, + &enabled, ) if updateErr != nil { log.WithFields(f).WithError(updateErr).Warn("unable to update existing github organization record") @@ -122,7 +126,7 @@ func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectS // we could simply update the record we initially loaded or simply query the updated record again... // we're using a key lookup, so it should be fast... - existingUpdatedRecord, getUpdatedRecordErr := repo.GetGithubOrganizationByName(ctx, utils.StringValue(input.OrganizationName)) + existingUpdatedRecord, getUpdatedRecordErr := repo.GetGitHubOrganizationByName(ctx, utils.StringValue(input.OrganizationName)) if getUpdatedRecordErr != nil { log.WithFields(f).WithError(getUpdatedRecordErr).Warn("unable to locate existing github organization by name") return nil, getUpdatedRecordErr @@ -141,6 +145,7 @@ func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectS // No existing records - create one _, currentTime := utils.CurrentTime() + enabled := true githubOrg := &GithubOrganization{ DateCreated: currentTime, DateModified: currentTime, @@ -149,6 +154,7 @@ func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectS OrganizationNameLower: strings.ToLower(*input.OrganizationName), OrganizationSFID: parentProjectSFID, ProjectSFID: projectSFID, + Enabled: aws.BoolValue(&enabled), AutoEnabled: aws.BoolValue(input.AutoEnabled), AutoEnabledClaGroupID: input.AutoEnabledClaGroupID, BranchProtectionEnabled: aws.BoolValue(input.BranchProtectionEnabled), @@ -183,10 +189,10 @@ func (repo repository) AddGithubOrganization(ctx context.Context, parentProjectS return ToModel(githubOrg), nil } -// GetGithubOrganizations get github organizations based on the project SFID -func (repo repository) GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { +// GetGitHubOrganizations get github organizations based on the project SFID +func (repo Repository) GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { f := logrus.Fields{ - "functionName": "GetGitHubOrganizations", + "functionName": "v1.github_organizations.repository.GetGitHubOrganizations", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, } @@ -194,6 +200,9 @@ func (repo repository) GetGithubOrganizations(ctx context.Context, projectSFID s condition := expression.Key("project_sfid").Equal(expression.Value(projectSFID)) builder := expression.NewBuilder().WithKeyCondition(condition) + filter := expression.Name("enabled").Equal(expression.Value(true)) + builder = builder.WithFilter(filter) + // Use the nice builder to create the expression expr, err := builder.Build() if err != nil { @@ -236,9 +245,10 @@ func (repo repository) GetGithubOrganizations(ctx context.Context, projectSFID s return &models.GithubOrganizations{List: ghOrgList}, nil } -func (repo repository) GetGithubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) { +// GetGitHubOrganizationsByParent returns a list of github organizations by parent project SFID +func (repo Repository) GetGitHubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) { f := logrus.Fields{ - "functionName": "GetGitHubOrganizations", + "functionName": "v1.github_organizations.repository.GetGitHubOrganizationsByParent", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "parentProjectSFID": parentProjectSFID, } @@ -287,9 +297,10 @@ func (repo repository) GetGithubOrganizationsByParent(ctx context.Context, paren return &models.GithubOrganizations{List: ghOrgList}, nil } -func (repo repository) GetGithubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) { +// GetGitHubOrganizationByName get github organization by name +func (repo Repository) GetGitHubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) { f := logrus.Fields{ - "functionName": "GetGitHubOrganizationByName", + "functionName": "v1.github_organizations.repository.GetGitHubOrganizationByName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "githubOrganizationName": githubOrganizationName, } @@ -335,9 +346,10 @@ func (repo repository) GetGithubOrganizationByName(ctx context.Context, githubOr return &models.GithubOrganizations{List: ghOrgList}, nil } -func (repo repository) GetGithubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) { +// GetGitHubOrganization by organization name +func (repo Repository) GetGitHubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) { f := logrus.Fields{ - "functionName": "GetGitHubOrganization", + "functionName": "v1.github_organizations.repository.GetGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "githubOrganizationName": githubOrganizationName, } @@ -368,10 +380,59 @@ func (repo repository) GetGithubOrganization(ctx context.Context, githubOrganiza return ToModel(&org), nil } -// UpdateGithubOrganization updates the specified GitHub organization based on the update model provided -func (repo repository) UpdateGithubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error { +func (repo Repository) getGithubOrganization(ctx context.Context, projectSFID string, organizationName string) ([]*models.GithubOrganization, error) { + f := logrus.Fields{ + "functionName": "v1.github_organizations.repository.getGithubOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "organizationName": organizationName, + } + + log.WithFields(f).Debug("Querying for github organization by project and name...") + + filter := expression.Key("project_sfid").Equal(expression.Value(projectSFID)).And(expression.Key("organization_name").Equal(expression.Value(organizationName))) + + expr, err := expression.NewBuilder().WithKeyCondition(filter).Build() + if err != nil { + log.WithFields(f).Warnf("problem building query expression, error: %+v", err) + return nil, err + } + + params := &dynamodb.QueryInput{ + TableName: aws.String(repo.githubOrgTableName), + IndexName: aws.String(ProjectSFIDOrganizationNameIndex), + KeyConditionExpression: expr.KeyCondition(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + } + + results, queryErr := repo.dynamoDBClient.Query(params) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving github organization using project_sfid = %s and organization_name = %s, error: %+v", projectSFID, organizationName, queryErr) + return nil, queryErr + } + + if len(results.Items) == 0 { + log.WithFields(f).Debug("no results from query") + return nil, nil + } + + var resultOutput []*GithubOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + return nil, err + } + + log.WithFields(f).Debug("building response model...") + ghOrgList := buildGithubOrganizationListModels(ctx, resultOutput) + + return ghOrgList, nil +} + +// UpdateGitHubOrganization updates the specified GitHub organization based on the update model provided +func (repo Repository) UpdateGitHubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool, enabled *bool) error { f := logrus.Fields{ - "functionName": "UpdateGitHubOrganization", + "functionName": "v1.github_organizations.repository.UpdateGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "organizationName": organizationName, @@ -382,7 +443,7 @@ func (repo repository) UpdateGithubOrganization(ctx context.Context, projectSFID } _, currentTime := utils.CurrentTime() - githubOrg, lookupErr := repo.GetGithubOrganization(ctx, organizationName) + githubOrg, lookupErr := repo.GetGitHubOrganization(ctx, organizationName) if lookupErr != nil { log.WithFields(f).Warnf("error looking up GitHub organization by name, error: %+v", lookupErr) return lookupErr @@ -393,37 +454,49 @@ func (repo repository) UpdateGithubOrganization(ctx context.Context, projectSFID return lookupErr } + expressionAttributeNames := map[string]*string{ + "#A": aws.String("auto_enabled"), + "#C": aws.String("auto_enabled_cla_group_id"), + "#B": aws.String("branch_protection_enabled"), + "#M": aws.String("date_modified"), + } + expressionAttributeValues := map[string]*dynamodb.AttributeValue{ + ":a": { + BOOL: aws.Bool(autoEnabled), + }, + ":c": { + S: aws.String(autoEnabledClaGroupID), + }, + ":b": { + BOOL: aws.Bool(branchProtectionEnabled), + }, + ":m": { + S: aws.String(currentTime), + }, + } + updateExpression := "SET #A = :a, #C = :c, #B = :b, #M = :m" + + if enabled != nil { + expressionAttributeNames["#E"] = aws.String("enabled") + expressionAttributeValues[":e"] = &dynamodb.AttributeValue{ + BOOL: aws.Bool(*enabled), + } + updateExpression = updateExpression + ", #E = :e " + } + input := &dynamodb.UpdateItemInput{ Key: map[string]*dynamodb.AttributeValue{ "organization_name": { S: aws.String(githubOrg.OrganizationName), }, }, - ExpressionAttributeNames: map[string]*string{ - "#A": aws.String("auto_enabled"), - "#C": aws.String("auto_enabled_cla_group_id"), - "#B": aws.String("branch_protection_enabled"), - "#M": aws.String("date_modified"), - }, - ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ - ":a": { - BOOL: aws.Bool(autoEnabled), - }, - ":c": { - S: aws.String(autoEnabledClaGroupID), - }, - ":b": { - BOOL: aws.Bool(branchProtectionEnabled), - }, - ":m": { - S: aws.String(currentTime), - }, - }, - UpdateExpression: aws.String("SET #A = :a, #C = :c, #B = :b, #M = :m"), - TableName: aws.String(repo.githubOrgTableName), + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(repo.githubOrgTableName), } - log.WithFields(f).Debug("updating github organization record...") + log.WithFields(f).Debugf("updating github organization record: %+v", input) _, updateErr := repo.dynamoDBClient.UpdateItem(input) if updateErr != nil { log.WithFields(f).Warnf("unable to update GitHub organization record, error: %+v", updateErr) @@ -433,56 +506,90 @@ func (repo repository) UpdateGithubOrganization(ctx context.Context, projectSFID return nil } -func (repo repository) DeleteGithubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error { +// DeleteGitHubOrganization deletes the github organization by project SFID +func (repo Repository) DeleteGitHubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error { f := logrus.Fields{ - "functionName": "DeleteGitHubOrganization", + "functionName": "v1.github_organizations.repository.DeleteGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "githubOrgName": githubOrgName, } var githubOrganizationName string - orgs, orgErr := repo.GetGithubOrganizations(ctx, projectSFID) + // orgs, orgErr := repo.GetGitHubOrganizations(ctx, projectSFID) + // if orgErr != nil { + // errMsg := fmt.Sprintf("github organization is not found using projectSFID: %s, error: %+v", projectSFID, orgErr) + // log.WithFields(f).Warn(errMsg) + // return errors.New(errMsg) + // } + + orgs, orgErr := repo.getGithubOrganization(ctx, projectSFID, githubOrgName) if orgErr != nil { errMsg := fmt.Sprintf("github organization is not found using projectSFID: %s, error: %+v", projectSFID, orgErr) log.WithFields(f).Warn(errMsg) return errors.New(errMsg) } - for _, githubOrg := range orgs.List { - if strings.EqualFold(githubOrg.OrganizationName, githubOrgName) { - githubOrganizationName = githubOrg.OrganizationName - } + if orgs == nil { + errMsg := fmt.Sprintf("github organization: %s is not found using projectSFID: %s", githubOrgName, projectSFID) + log.WithFields(f).Warn(errMsg) + return errors.New(errMsg) } - log.WithFields(f).Debug("Deleting GitHub organization...") - _, err := repo.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "organization_name": { - S: aws.String(githubOrganizationName), + for _, githubOrg := range orgs { + githubOrganizationName = githubOrg.OrganizationName + log.WithFields(f).Debugf("Deleting GitHub organization...: %s", githubOrganizationName) + // Update enabled flag as false + _, currentTime := utils.CurrentTime() + note := fmt.Sprintf("Enabled set to false due to org deletion at %s ", currentTime) + _, err := repo.dynamoDBClient.UpdateItem( + &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "organization_name": { + S: aws.String(githubOrganizationName), + }, + }, + ExpressionAttributeNames: map[string]*string{ + "#E": aws.String("enabled"), + "#N": aws.String("note"), + "#D": aws.String("date_modified"), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":e": { + BOOL: aws.Bool(false), + }, + ":n": { + S: aws.String(note), + }, + ":d": { + S: aws.String(currentTime), + }, + }, + UpdateExpression: aws.String("SET #E = :e, #N = :n, #D = :d"), + TableName: aws.String(repo.githubOrgTableName), }, - }, - TableName: aws.String(repo.githubOrgTableName), - }) - if err != nil { - errMsg := fmt.Sprintf("error deleting github organization: %s - %+v", githubOrgName, err) - log.WithFields(f).Warnf(errMsg) - return errors.New(errMsg) + ) + if err != nil { + errMsg := fmt.Sprintf("error deleting github organization: %s - %+v", githubOrgName, err) + log.WithFields(f).Warnf(errMsg) + return errors.New(errMsg) + } } return nil } -func (repo repository) DeleteGithubOrganizationByParent(ctx context.Context, parentProjectSFID string, githubOrgName string) error { +// DeleteGitHubOrganizationByParent deletes the github organization by parent SFID +func (repo Repository) DeleteGitHubOrganizationByParent(ctx context.Context, parentProjectSFID string, githubOrgName string) error { f := logrus.Fields{ - "functionName": "DeleteGitHubOrganization", + "functionName": "v1.github_organizations.repository.DeleteGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "parentProjectSFID": parentProjectSFID, "githubOrgName": githubOrgName, } var githubOrganizationName string - orgs, orgErr := repo.GetGithubOrganizationsByParent(ctx, parentProjectSFID) + orgs, orgErr := repo.GetGitHubOrganizationsByParent(ctx, parentProjectSFID) if orgErr != nil { errMsg := fmt.Sprintf("github organization is not found using parentProjectSFID %s, error: - %+v", parentProjectSFID, orgErr) log.WithFields(f).Warn(errMsg) diff --git a/cla-backend-go/github_organizations/service.go b/cla-backend-go/github_organizations/service.go index 562d7b9b0..02d3b4109 100644 --- a/cla-backend-go/github_organizations/service.go +++ b/cla-backend-go/github_organizations/service.go @@ -15,36 +15,39 @@ import ( log "github.com/communitybridge/easycla/cla-backend-go/logging" "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/repositories" ) -// Service contains functions of GithubOrganizations service -type Service interface { - AddGithubOrganization(ctx context.Context, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) - GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) - GetGithubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) - GetGithubOrganizationByName(ctx context.Context, githubOrgName string) (*models.GithubOrganization, error) - UpdateGithubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error - DeleteGithubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error +// ServiceInterface contains functions of GithubOrganizations service +type ServiceInterface interface { + AddGitHubOrganization(ctx context.Context, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) + GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) + GetGitHubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) + GetGitHubOrganizationByName(ctx context.Context, githubOrgName string) (*models.GithubOrganization, error) + UpdateGitHubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error + DeleteGitHubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error + RemoveDuplicates(input []*models.GithubOrganization) []*models.GithubOrganization } -type service struct { - repo Repository - ghRepository repositories.Repository +// Service object/struct +type Service struct { + repo RepositoryInterface + ghRepository repositories.RepositoryInterface claRepository projects_cla_groups.Repository } // NewService creates a new githubOrganizations service -func NewService(repo Repository, ghRepository repositories.Repository, claRepository projects_cla_groups.Repository) Service { - return service{ +func NewService(repo RepositoryInterface, ghRepository repositories.RepositoryInterface, claRepository projects_cla_groups.Repository) Service { + return Service{ repo: repo, ghRepository: ghRepository, claRepository: claRepository, } } -func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, input *models.CreateGithubOrganization) (*models.GithubOrganization, error) { +// AddGitHubOrganization adds the GitHub organization for the specified project +func (s Service) AddGitHubOrganization(ctx context.Context, projectSFID string, input *models.GithubCreateOrganization) (*models.GithubOrganization, error) { f := logrus.Fields{ "functionName": "AddGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -59,63 +62,65 @@ func (s service) AddGithubOrganization(ctx context.Context, projectSFID string, log.WithFields(f).Warnf("problem fetching github organizations by projectSFID, error: %+v", projErr) return nil, projErr } + if parentProjectSFID == "" { + parentProjectSFID = projectSFID + } // check if valid cla group id is passed if input.AutoEnabledClaGroupID != "" { - if _, err := s.claRepository.GetCLAGroupNameByID(input.AutoEnabledClaGroupID); err != nil { + if _, err := s.claRepository.GetCLAGroupNameByID(ctx, input.AutoEnabledClaGroupID); err != nil { return nil, err } } - return s.repo.AddGithubOrganization(ctx, parentProjectSFID, projectSFID, input) + return s.repo.AddGitHubOrganization(ctx, parentProjectSFID, projectSFID, input) } -func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { +// GetGitHubOrganizations returns the GitHub organization for the specified project +func (s Service) GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { f := logrus.Fields{ "functionName": "GetGitHubOrganizations", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, } - gitHubOrgModels, err := s.repo.GetGithubOrganizations(ctx, projectSFID) + // track githubOrgs based on parent/child anchor + var gitHubOrgModels = models.GithubOrganizations{} + var githubOrgs = make([]*models.GithubOrganization, 0) + + projectGithubModels, err := s.repo.GetGitHubOrganizations(ctx, projectSFID) if err != nil { log.WithFields(f).Warnf("problem fetching github organizations by projectSFID, error: %+v", err) return nil, err } - if len(gitHubOrgModels.List) >= 0 { - return gitHubOrgModels, err + if len(projectGithubModels.List) >= 0 { + githubOrgs = append(githubOrgs, projectGithubModels.List...) } + log.WithFields(f).Debugf("loaded %d GitHub organizations using projectSFID: %s", len(projectGithubModels.List), projectSFID) - log.WithFields(f).Debug("unable to find github organizations by projectSFID - searching by parent...") - // Lookup the parent - parentProjectSFID, projErr := v2ProjectService.GetClient().GetParentProject(projectSFID) - if projErr != nil { - log.WithFields(f).Warnf("problem fetching project parent SFID, error: %+v", projErr) - return nil, projErr - } + gitHubOrgModels.List = githubOrgs - if parentProjectSFID != projectSFID { - log.WithFields(f).Debugf("searching github organization by parent SFID: %s", parentProjectSFID) - return s.repo.GetGithubOrganizationsByParent(ctx, parentProjectSFID) - } + // Remove potential duplicates + gitHubOrgModels.List = s.RemoveDuplicates(gitHubOrgModels.List) - log.WithFields(f).Debugf("no parent or parent is %s - search criteria exhausted", utils.TheLinuxFoundation) - return gitHubOrgModels, err + return &gitHubOrgModels, err } -func (s service) GetGithubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) { - return s.repo.GetGithubOrganizationsByParent(ctx, parentProjectSFID) +// GetGitHubOrganizationsByParent returns the GitHub organizations for the specified parent project SFID +func (s Service) GetGitHubOrganizationsByParent(ctx context.Context, parentProjectSFID string) (*models.GithubOrganizations, error) { + return s.repo.GetGitHubOrganizationsByParent(ctx, parentProjectSFID) } -func (s service) GetGithubOrganizationByName(ctx context.Context, githubOrgName string) (*models.GithubOrganization, error) { +// GetGitHubOrganizationByName returns the GitHub organizations for the specified GitHub organization name +func (s Service) GetGitHubOrganizationByName(ctx context.Context, githubOrgName string) (*models.GithubOrganization, error) { f := logrus.Fields{ "functionName": "GetGitHubOrganizationByName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "githubOrgName": githubOrgName, } - gitHubOrgs, err := s.repo.GetGithubOrganizationByName(ctx, githubOrgName) + gitHubOrgs, err := s.repo.GetGitHubOrganizationByName(ctx, githubOrgName) if err != nil { log.WithFields(f).Warnf("problem fetching github organizations by name, error: %+v", err) return nil, err @@ -132,17 +137,19 @@ func (s service) GetGithubOrganizationByName(ctx context.Context, githubOrgName return gitHubOrgs.List[0], err } -func (s service) UpdateGithubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error { +// UpdateGitHubOrganization updates the specified github organization based on the project SFID, organization name provided values +func (s Service) UpdateGitHubOrganization(ctx context.Context, projectSFID string, organizationName string, autoEnabled bool, autoEnabledClaGroupID string, branchProtectionEnabled bool) error { // check if valid cla group id is passed if autoEnabledClaGroupID != "" { - if _, err := s.claRepository.GetCLAGroupNameByID(autoEnabledClaGroupID); err != nil { + if _, err := s.claRepository.GetCLAGroupNameByID(ctx, autoEnabledClaGroupID); err != nil { return err } } - 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 { +// DeleteGitHubOrganization removes the specified github organization under the projectSFID +func (s Service) DeleteGitHubOrganization(ctx context.Context, projectSFID string, githubOrgName string) error { f := logrus.Fields{ "functionName": "DeleteGitHubOrganization", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -156,12 +163,33 @@ func (s service) DeleteGithubOrganization(ctx context.Context, projectSFID strin log.WithFields(f).Warnf("problem fetching project parent SFID, error: %+v", projErr) return projErr } + if parentProjectSFID == "" { + parentProjectSFID = projectSFID + } - err := s.ghRepository.DisableRepositoriesOfGithubOrganization(ctx, parentProjectSFID, githubOrgName) + err := s.ghRepository.GitHubDisableRepositoriesOfOrganization(ctx, parentProjectSFID, githubOrgName) if err != nil { log.WithFields(f).Warnf("problem disabling repositories for github organizations, error: %+v", projErr) return err } - return s.repo.DeleteGithubOrganization(ctx, projectSFID, githubOrgName) + return s.repo.DeleteGitHubOrganization(ctx, projectSFID, githubOrgName) +} + +// RemoveDuplicates removes any duplicates from the specified list +func (s Service) RemoveDuplicates(input []*models.GithubOrganization) []*models.GithubOrganization { + if input == nil { + return nil + } + keys := make(map[string]bool) + + output := []*models.GithubOrganization{} + for _, ghOrg := range input { + if _, value := keys[ghOrg.OrganizationName]; !value { + keys[ghOrg.OrganizationName] = true + output = append(output, ghOrg) + } + } + + return output } diff --git a/cla-backend-go/gitlab_api/auth.go b/cla-backend-go/gitlab_api/auth.go new file mode 100644 index 000000000..87664d459 --- /dev/null +++ b/cla-backend-go/gitlab_api/auth.go @@ -0,0 +1,111 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "errors" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/config" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/go-resty/resty/v2" + "github.com/sirupsen/logrus" +) + +const oauthURL = "https://gitlab.com/oauth/token" + +// RefreshOauthToken common routine to refresh the GitLab token +func RefreshOauthToken(refreshToken string) (*OauthSuccessResponse, error) { + gitLabConfig := config.GetConfig().Gitlab + f := logrus.Fields{ + "functionName": "gitlab.auth.RefreshOauthToken", + "refreshToken": refreshToken, + } + + if len(gitLabConfig.AppClientID) > 4 { + f["gitLabClientID"] = fmt.Sprintf("%s...%s", gitLabConfig.AppClientID[0:4], gitLabConfig.AppClientID[len(gitLabConfig.AppClientID)-4:]) + } else { + return nil, errors.New("gitlab application client ID value is not set - value is empty or malformed") + } + if len(gitLabConfig.AppClientSecret) > 4 { + f["gitLabClientSecret"] = fmt.Sprintf("%s...%s", gitLabConfig.AppClientSecret[0:4], gitLabConfig.AppClientSecret[len(gitLabConfig.AppClientSecret)-4:]) + } else { + return nil, errors.New("gitlab application client secret value is not set - value is empty or malformed") + } + + // For info on this authorization flow, see: https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow + client := resty.New() + params := map[string]string{ + "client_id": gitLabConfig.AppClientID, + "client_secret": gitLabConfig.AppClientSecret, + "refresh_token": refreshToken, + "grant_type": "refresh_token", + "redirect_uri": gitLabConfig.RedirectURI, + //"redirect_uri": "http://localhost:8080/v4/gitlab/oauth/callback", + } + resp, err := client.R(). + SetQueryParams(params). + SetResult(&OauthSuccessResponse{}). + Post(oauthURL) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error fetching oauth credentials from gitlab") + return nil, err + } + + if resp.StatusCode() != 200 { + log.WithFields(f).Warnf("error fetching oauth credentials from gitlab - status code: %d", resp.StatusCode()) + return nil, errors.New("error fetching oauth credentials from gitlab") + } + + return resp.Result().(*OauthSuccessResponse), nil +} + +// FetchOauthCredentials is responsible for fetching the credentials from gitlab for alredy started Oauth process (access_token, refresh_token) +func FetchOauthCredentials(code string) (*OauthSuccessResponse, error) { + gitLabConfig := config.GetConfig().Gitlab + f := logrus.Fields{ + "functionName": "gitlab.auth.FetchOauthCredentials", + "code": code, + "redirectURI": config.GetConfig().Gitlab.RedirectURI, + } + + if len(gitLabConfig.AppClientID) > 4 { + f["gitLabClientID"] = fmt.Sprintf("%s...%s", gitLabConfig.AppClientID[0:4], gitLabConfig.AppClientID[len(gitLabConfig.AppClientID)-4:]) + } else { + return nil, errors.New("gitlab application client ID value is not set - value is empty or malformed") + } + if len(gitLabConfig.AppClientSecret) > 4 { + f["gitLabClientSecret"] = fmt.Sprintf("%s...%s", gitLabConfig.AppClientSecret[0:4], gitLabConfig.AppClientSecret[len(gitLabConfig.AppClientSecret)-4:]) + } else { + return nil, errors.New("gitlab application client secret value is not set - value is empty or malformed") + } + + // For info on this authorization flow, see: https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow + client := resty.New() + params := map[string]string{ + "client_id": gitLabConfig.AppClientID, + "client_secret": gitLabConfig.AppClientSecret, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": gitLabConfig.RedirectURI, + //"redirect_uri": "http://localhost:8080/v4/gitlab/oauth/callback", + } + + resp, err := client.R(). + SetQueryParams(params). + SetResult(&OauthSuccessResponse{}). + Post(oauthURL) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem invoking GitLab auth token exchange to: %s", oauthURL) + return nil, err + } + + if resp.StatusCode() < 200 || resp.StatusCode() > 299 { + msg := fmt.Sprintf("problem invoking GitLab auth token exchange to: %s with status code: %d, response: %s", oauthURL, resp.StatusCode(), string(resp.Body())) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + + return resp.Result().(*OauthSuccessResponse), nil +} diff --git a/cla-backend-go/gitlab_api/branch_protection.go b/cla-backend-go/gitlab_api/branch_protection.go new file mode 100644 index 000000000..a3f90d398 --- /dev/null +++ b/cla-backend-go/gitlab_api/branch_protection.go @@ -0,0 +1,120 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "context" + "fmt" + + 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" +) + +// SetOrCreateBranchProtection sets the required parameters if existing pattern exists or creates a new one +func SetOrCreateBranchProtection(ctx context.Context, client *gitlab.Client, projectID int, protectionPattern string) error { + var err error + f := logrus.Fields{ + "functionName": "gitlab_api.SetOrCreateBranchProtection", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabProjectID": projectID, + "protectionPattern": protectionPattern, + } + + log.WithFields(f).Debugf("setting branch protection...") + + 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.WithFields(f).Debugf("branch protection already set nothing to do") + return nil + } + + //it's an existing one try to remove it first and re-create it + log.WithFields(f).Debugf("removing old branch protection") + _, err = client.ProtectedBranches.UnprotectRepositoryBranches(projectID, protectionPattern) + if err != nil { + return fmt.Errorf("removing protection for existing branch failed : %v", err) + } + } + + log.WithFields(f).Debugf("re-creating branch protection ") + if _, err = createBranchProtection(client, projectID, protectionPattern); err != nil { + return fmt.Errorf("recreating branch protection failed : %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 { + 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 +} + +// GetDefaultBranch 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/gitlab_api/client.go b/cla-backend-go/gitlab_api/client.go new file mode 100644 index 000000000..9566aefeb --- /dev/null +++ b/cla-backend-go/gitlab_api/client.go @@ -0,0 +1,156 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + + goGitLab "github.com/xanzy/go-gitlab" +) + +// OauthSuccessResponse is success response from Gitlab +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"` +} + +// NewGitlabOauthClient creates a new gitlab client from the given oauth info, authInfo is encrypted +func NewGitlabOauthClient(authInfo string, gitLabApp *App) (*goGitLab.Client, error) { + if authInfo == "" { + return nil, errors.New("unable to decrypt auth info - authentication info input is nil") + } + if gitLabApp == nil || gitLabApp.gitLabAppID == "" || gitLabApp.gitLabAppPrivateKey == "" || gitLabApp.gitLabAppSecret == "" { + return nil, errors.New("unable to decrypt auth info - GitLab app structure is nil or empty") + } + + oauthResp, err := DecryptAuthInfo(authInfo, gitLabApp) + if err != nil { + return nil, err + } + + if oauthResp == nil { + return nil, errors.New("unable to decrypt auth info - value is nil") + } + + log.Infof("creating oauth client with access token : %s", oauthResp.AccessToken) + return goGitLab.NewOAuthClient(oauthResp.AccessToken) +} + +// NewGitlabOauthClientFromAccessToken creates a new gitlab client from the given access token +func NewGitlabOauthClientFromAccessToken(accessToken string) (*goGitLab.Client, error) { + return goGitLab.NewOAuthClient(accessToken) +} + +// EncryptAuthInfo encrypts the oauth response into a string +func EncryptAuthInfo(oauthResp *OauthSuccessResponse, gitLabApp *App) (string, error) { + keyDecoded, err := base64.StdEncoding.DecodeString(gitLabApp.GetAppPrivateKey()) + if err != nil { + return "", fmt.Errorf("problem decoding GitLab private glClientKey, error: %v", err) + } + + b, err := json.Marshal(oauthResp) + if err != nil { + return "", fmt.Errorf("problem marshalling oauth resp json, error: %v", err) + } + authInfo := string(b) + //log.Infof("auth info before encrypting : %s", authInfo) + + encrypted, err := encrypt(keyDecoded, []byte(authInfo)) + if err != nil { + return "", fmt.Errorf("encrypt failed : %v", err) + } + + return hex.EncodeToString(encrypted), nil +} + +// DecryptAuthInfo decrypts the auth info into OauthSuccessResponse data structure +func DecryptAuthInfo(authInfoEncoded string, gitLabApp *App) (*OauthSuccessResponse, error) { + ciphertext, err := hex.DecodeString(authInfoEncoded) + if err != nil { + return nil, fmt.Errorf("decode auth info %s : %v", authInfoEncoded, err) + } + + //log.Infof("auth info decoded : %s", ciphertext) + + keyDecoded, err := base64.StdEncoding.DecodeString(gitLabApp.GetAppPrivateKey()) + if err != nil { + return nil, fmt.Errorf("decode glClientKey : %v", err) + } + + //log.Debugf("before decrypt : keyDecoded : %s, cipherText : %s", keyDecoded, ciphertext) + decrypted, err := decrypt(keyDecoded, ciphertext) + if err != nil { + return nil, fmt.Errorf("decrypt failed : %v", err) + } + //log.Debugf("after decrypt : keyDecoded : %s, decrypted : %s", keyDecoded, decrypted) + + var oauthResp OauthSuccessResponse + if err := json.Unmarshal(decrypted, &oauthResp); err != nil { + return nil, fmt.Errorf("unmarshall auth info : %v", err) + } + + return &oauthResp, nil +} + +func encrypt(key, message []byte) ([]byte, error) { + // Initialize block cipher + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // Create the byte slice that will hold encrypted message + cipherText := make([]byte, aes.BlockSize+len(message)) + + // Generate the Initialization Vector (IV) nonce + // which is stored at the beginning of the byte slice + // The IV is the same length as the AES blocksize + iv := cipherText[:aes.BlockSize] + _, err = io.ReadFull(rand.Reader, iv) + if err != nil { + return nil, err + } + + // Choose the block cipher mode of operation + // Using the cipher feedback (CFB) mode here. + // CBCEncrypter also available. + cfb := cipher.NewCFBEncrypter(block, iv) + // Generate the encrypted message and store it + // in the remaining bytes after the IV nonce + cfb.XORKeyStream(cipherText[aes.BlockSize:], message) + + return cipherText, nil +} + +// AES decryption +func decrypt(key, cipherText []byte) ([]byte, error) { + // Initialize block cipher + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // Separate the IV nonce from the encrypted message bytes + iv := cipherText[:aes.BlockSize] + cipherText = cipherText[aes.BlockSize:] + + // Decrypt the message using the CFB block mode + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(cipherText, cipherText) + + return cipherText, nil +} diff --git a/cla-backend-go/gitlab_api/client_groups.go b/cla-backend-go/gitlab_api/client_groups.go new file mode 100644 index 000000000..edbb249bf --- /dev/null +++ b/cla-backend-go/gitlab_api/client_groups.go @@ -0,0 +1,267 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "context" + "errors" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + + goGitLab "github.com/xanzy/go-gitlab" +) + +// UserGroup represents gitlab group +type UserGroup struct { + Name string + FullPath string +} + +// GetGroupsListAll returns a complete list of GitLab groups for which the client as authorization/visibility +func GetGroupsListAll(ctx context.Context, client *goGitLab.Client, minAccessLevel goGitLab.AccessLevelValue) ([]*goGitLab.Group, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.GetGroupsListAll", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + // https://docs.gitlab.com/ee/api/groups.html#list-groups + // Query GitLab for repos - fetch the list of repositories available to the GitLab App + listGroupsOpts := &goGitLab.ListGroupsOptions{ + ListOptions: goGitLab.ListOptions{ + Page: 1, // starts with one: https://docs.gitlab.com/ee/api/#offset-based-pagination + PerPage: 100, // max is 100 + }, + AllAvailable: utils.Bool(true), // Show all the groups you have access to (defaults to false for authenticated users, true for administrators); Attributes owned and min_access_level have precedence + MinAccessLevel: goGitLab.AccessLevel(minAccessLevel), // Limit by current user minimal access level. + } + + var groupList []*goGitLab.Group + for { + groups, resp, listGroupsErr := client.Groups.ListGroups(listGroupsOpts) + if listGroupsErr != nil { + msg := fmt.Sprintf("unable to list groups, error: %+v", listGroupsErr) + log.WithFields(f).WithError(listGroupsErr).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to list groups, status code: %d", resp.StatusCode) + log.WithFields(f).WithError(listGroupsErr).Warn(msg) + return nil, errors.New(msg) + } + + // Append to our response + groupList = append(groupList, groups...) + + // Do we have any records to process? + if resp.NextPage == 0 { + break + } + } + + return groupList, nil +} + +// GetGroupByName gets a gitlab Group by the given name +func GetGroupByName(ctx context.Context, client *goGitLab.Client, name string) (*goGitLab.Group, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.GetGroupByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + groups, resp, err := client.Groups.SearchGroup(name) + //groups, _, err := client.Groups.ListGroups(&goGitLab.ListGroupsOptions{}) + if err != nil { + msg := fmt.Sprintf("problem fetching groups, error: %+v", err) + log.WithFields(f).WithError(err).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to search groups using query: %s, status code: %d", name, resp.StatusCode) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + + for _, group := range groups { + log.WithFields(f).Debugf("testing %s == %s or %s", name, group.Name, group.FullPath) + if group.Name == name { + return group, nil + } + if group.FullPath == name { + return group, nil + } + } + + return nil, nil +} + +// GetGroupByID gets a gitlab Group by the given name +func GetGroupByID(ctx context.Context, client *goGitLab.Client, groupID int) (*goGitLab.Group, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.GetGroupByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + group, resp, err := client.Groups.GetGroup(groupID) + if err != nil { + msg := fmt.Sprintf("problem fetching group by ID: %d, error: %+v", groupID, err) + log.WithFields(f).WithError(err).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to find group by ID: %d, status code: %d", groupID, resp.StatusCode) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + + return group, nil +} + +// GetGroupByFullPath gets a gitlab Group by the given full path +func GetGroupByFullPath(ctx context.Context, client *goGitLab.Client, fullPath string) (*goGitLab.Group, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.GetGroupByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + groups, err := GetGroupsListAll(ctx, client, goGitLab.MaintainerPermissions) + //groups, _, err := client.Groups.ListGroups(&goGitLab.ListGroupsOptions{}) + if err != nil { + msg := fmt.Sprintf("problem fetching groups, error: %+v", err) + log.WithFields(f).WithError(err).Warn(msg) + return nil, errors.New(msg) + } + + for _, group := range groups { + log.WithFields(f).Debugf("testing %s == %s", fullPath, group.FullPath) + if group.FullPath == fullPath { + return group, nil + } + } + + return nil, nil +} + +// GetGroupProjectListByGroupID returns a list of GitLab projects under the specified Organization +func GetGroupProjectListByGroupID(ctx context.Context, client *goGitLab.Client, groupID int) ([]*goGitLab.Project, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.GetGroupProjectListByGroupID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + if groupID == 0 { + return nil, errors.New("invalid groupID value - 0") + } + + opts := &goGitLab.ListGroupProjectsOptions{ + ListOptions: goGitLab.ListOptions{ + Page: 1, // starts with one: https://docs.gitlab.com/ee/api/#offset-based-pagination + PerPage: 100, // max is 100 + }, + IncludeSubgroups: utils.Bool(true), // Include projects in subgroups of this group. Default is false + MinAccessLevel: goGitLab.AccessLevel(goGitLab.MaintainerPermissions), // Limit by current user minimal access level. + } + + var projectList []*goGitLab.Project + for { + // https://docs.gitlab.com/ee/api/groups.html#list-a-groups-projects + projects, resp, listProjectsErr := client.Groups.ListGroupProjects(groupID, opts) + if listProjectsErr != nil { + msg := fmt.Sprintf("unable to list projects, error: %+v", listProjectsErr) + log.WithFields(f).WithError(listProjectsErr).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to list projects, status code: %d", resp.StatusCode) + log.WithFields(f).WithError(listProjectsErr).Warn(msg) + return nil, errors.New(msg) + } + + // Append to our response + projectList = append(projectList, projects...) + + // Do we have any records to process? + if resp.NextPage == 0 { + break + } + } + + return projectList, nil +} + +// ListGroupMembers lists the members of a given groupID +func ListGroupMembers(ctx context.Context, client *goGitLab.Client, groupID int) ([]*goGitLab.GroupMember, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.GetGroupMembers", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + log.WithFields(f).Debugf("fetching gitlab members for groupID: %d", groupID) + + opts := &goGitLab.ListGroupMembersOptions{} + members, _, err := client.Groups.ListGroupMembers(groupID, opts) + if err != nil { + log.WithFields(f).Debugf("unable to fetch members for gitlab GroupID : %d", groupID) + return nil, err + } + return members, err +} + +// ListUserProjectGroups fetches the unique groups of a gitlab users groups, +// note: it doesn't list the projects/groups the user is member of ..., it's very limited +func ListUserProjectGroups(ctx context.Context, client *goGitLab.Client, userID int) ([]*UserGroup, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_groups.ListUserProjectGroups", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + listOptions := &goGitLab.ListProjectsOptions{ + ListOptions: goGitLab.ListOptions{ + PerPage: 100, + }} + + userGroupsMap := map[string]*UserGroup{} + for { + log.WithFields(f).Debugf("fetching projects for user id : %d with options : %v", userID, listOptions.ListOptions) + projects, resp, err := client.Projects.ListUserProjects(userID, listOptions) + if err != nil { + msg := fmt.Sprintf("listing user : %d projects failed : %v", userID, err) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to list user projects using userID: %d, status code: %d", userID, resp.StatusCode) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + log.Debugf("fetched %d projects for the user ", len(projects)) + + if len(projects) == 0 { + break + } + + for _, p := range projects { + log.Debugf("checking following project : %s", p.PathWithNamespace) + log.Debugf("fetched following namespace : %+v", p.Namespace) + userGroupsMap[p.Namespace.FullPath] = &UserGroup{ + Name: p.Namespace.Name, + FullPath: p.Namespace.FullPath, + } + } + + if listOptions.Page >= resp.NextPage { + break + } + listOptions.Page = resp.NextPage + } + + var userGroups []*UserGroup + for _, v := range userGroupsMap { + userGroups = append(userGroups, v) + } + + return userGroups, nil +} diff --git a/cla-backend-go/gitlab_api/client_projects.go b/cla-backend-go/gitlab_api/client_projects.go new file mode 100644 index 000000000..ec8fcd4a6 --- /dev/null +++ b/cla-backend-go/gitlab_api/client_projects.go @@ -0,0 +1,146 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "context" + "errors" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + + goGitLab "github.com/xanzy/go-gitlab" +) + +// GetProjectListAll returns a complete list of GitLab projects for which the client as authorization/visibility +func GetProjectListAll(ctx context.Context, client *goGitLab.Client) ([]*goGitLab.Project, error) { + // https://docs.gitlab.com/ce/api/projects.html#list-projects + // Query GitLab for repos - fetch the list of repositories available to the GitLab App + listProjectsOpts := &goGitLab.ListProjectsOptions{ + ListOptions: goGitLab.ListOptions{ + Page: 1, // starts with one: https://docs.gitlab.com/ee/api/#offset-based-pagination + PerPage: 100, // max is 100 + }, + SearchNamespaces: utils.Bool(true), // Include ancestor namespaces when matching search criteria. Default is false. + Membership: utils.Bool(true), // Limit by projects that the current user is a member of. + MinAccessLevel: goGitLab.AccessLevel(goGitLab.MaintainerPermissions), // Limit by current user minimal access level. + } + + return getProjectListWithOptions(ctx, client, listProjectsOpts) +} + +// GetProjectListByOrgName returns a list of GitLab projects under the specified Organization +func GetProjectListByOrgName(ctx context.Context, client *goGitLab.Client, organizationName string) ([]*goGitLab.Project, error) { + // Query GitLab for repos - fetch the list of repositories available to the GitLab App + listProjectsOpts := &goGitLab.ListProjectsOptions{ + ListOptions: goGitLab.ListOptions{ + Page: 1, // starts with one: https://docs.gitlab.com/ee/api/#offset-based-pagination + PerPage: 100, // max is 100 + }, + Search: utils.StringRef(organizationName), // filter by our organization name + SearchNamespaces: utils.Bool(true), // Include ancestor namespaces when matching search criteria. Default is false. + Membership: utils.Bool(true), // Limit by projects that the current user is a member of. + MinAccessLevel: goGitLab.AccessLevel(goGitLab.MaintainerPermissions), // Limit by current user minimal access level. + } + + return getProjectListWithOptions(ctx, client, listProjectsOpts) +} + +// getProjectListWithOptions returns a list of GitLab projects using the specified filter +func getProjectListWithOptions(ctx context.Context, client *goGitLab.Client, opts *goGitLab.ListProjectsOptions) ([]*goGitLab.Project, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_projects.getProjectListWithOptions", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + var projectList []*goGitLab.Project + for { + // 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 := client.Projects.ListProjects(opts) + if listProjectsErr != nil { + msg := fmt.Sprintf("unable to list projects, error: %+v", listProjectsErr) + log.WithFields(f).WithError(listProjectsErr).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to list projects, status code: %d", resp.StatusCode) + log.WithFields(f).WithError(listProjectsErr).Warn(msg) + return nil, errors.New(msg) + } + + // Append to our response + projectList = append(projectList, projects...) + + // Do we have any records to process? + if resp.NextPage == 0 { + break + } + } + + return projectList, nil +} + +// GetProjectByID returns the GitLab project for the specified ID +func GetProjectByID(ctx context.Context, client *goGitLab.Client, gitLabProjectID int) (*goGitLab.Project, error) { + f := logrus.Fields{ + "functionName": "gitlab.client.GetProjectByID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabProjectID": gitLabProjectID, + } + + // Query GitLab for repos - fetch the list of repositories available to the GitLab App + project, resp, getProjectErr := client.Projects.GetProject(gitLabProjectID, &goGitLab.GetProjectOptions{}) + if getProjectErr != nil { + msg := fmt.Sprintf("unable to get project by ID: %d, error: %+v", gitLabProjectID, getProjectErr) + log.WithFields(f).WithError(getProjectErr).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to get project by ID: %d, status code: %d", gitLabProjectID, resp.StatusCode) + log.WithFields(f).WithError(getProjectErr).Warn(msg) + return nil, errors.New(msg) + } + if project == nil { + msg := fmt.Sprintf("unable to get project by ID: %d, project is empty", gitLabProjectID) + log.WithFields(f).WithError(getProjectErr).Warn(msg) + return nil, errors.New(msg) + } + + return project, nil +} + +// EnableMergePipelineProtection enables the pipeline protection on given project, by default it's +// turned off and when a new MR is raised users can merge requests bypassing the pipelines. With this +// setting gitlab disables the Merge button if any of the pipelines are failing +func EnableMergePipelineProtection(ctx context.Context, gitlabClient *goGitLab.Client, projectID int) error { + f := logrus.Fields{ + "functionName": "gitlab.client.EnableMergePipelineProtection", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabProjectID": projectID, + } + + project, _, err := gitlabClient.Projects.GetProject(projectID, &goGitLab.GetProjectOptions{}) + if err != nil { + return fmt.Errorf("fetching project failed : %v", err) + } + + log.WithFields(f).Debugf("Merge if Pipeline is succeeds flag enabled : %v", project.OnlyAllowMergeIfPipelineSucceeds) + if project.OnlyAllowMergeIfPipelineSucceeds { + return nil + } + + project.OnlyAllowMergeIfPipelineSucceeds = true + log.WithFields(f).Debugf("Enabling Merge Pipeline protection") + _, _, err = gitlabClient.Projects.EditProject(projectID, &goGitLab.EditProjectOptions{ + OnlyAllowMergeIfPipelineSucceeds: goGitLab.Bool(true), + }) + + if err != nil { + return fmt.Errorf("editing project : %d failed : %v", projectID, err) + } + return nil +} diff --git a/cla-backend-go/gitlab_api/client_test.go b/cla-backend-go/gitlab_api/client_test.go new file mode 100644 index 000000000..7721d3b60 --- /dev/null +++ b/cla-backend-go/gitlab_api/client_test.go @@ -0,0 +1,61 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +var glClientID = "124453345" +var glClientSecret = "124453345" +var glClientKey = "0WqnDWHnZKo2cmQ8m93EtY9ZBpfzQW4UnnEuRmgtJKM=" +var oauthRespStr = `{"access_token":"a30671b8749ba5d48925712344377f11a5aba43ec630f099e464b9843796e6a6","token_type":"Bearer","expires_in":0,"refresh_token":"0838a31d0d796973eacefdf513523e6e47aa06fac9d26622964da1e473509458","created_at":1626435922}` + +const GitLabTestsEnabled = false + +func TestNewGitlabOauthClient(t *testing.T) { + if GitLabTestsEnabled { + gitLabApp := Init(glClientID, glClientSecret, glClientKey) + + t.Logf("app private ID is : %s", gitLabApp.GetAppID()) + t.Logf("app private key is : %s", gitLabApp.GetAppPrivateKey()) + + var oauthResp OauthSuccessResponse + err := json.Unmarshal([]byte(oauthRespStr), &oauthResp) + assert.NoError(t, err) + + encrypted, err := EncryptAuthInfo(&oauthResp, gitLabApp) + assert.NoError(t, err) + + client, err := NewGitlabOauthClient(encrypted, gitLabApp) + assert.NoError(t, err) + assert.NotNil(t, client) + } +} + +func TestEncryptDecryptAuthInfo(t *testing.T) { + if GitLabTestsEnabled { + gitLabApp := Init(glClientID, glClientSecret, glClientKey) + + t.Logf("app private ID is : %s", gitLabApp.GetAppID()) + t.Logf("app private key is : %s", gitLabApp.GetAppPrivateKey()) + + var oauthResp OauthSuccessResponse + err := json.Unmarshal([]byte(oauthRespStr), &oauthResp) + assert.NoError(t, err) + t.Logf("unmarshall ok : %+v", oauthResp) + + encrypted, err := EncryptAuthInfo(&oauthResp, gitLabApp) + assert.NoError(t, err) + t.Logf("encrypted auth info : %s", encrypted) + + oauthRespDecrypted, err := DecryptAuthInfo(encrypted, gitLabApp) + assert.NoError(t, err) + + assert.Equal(t, &oauthResp, oauthRespDecrypted) + } +} diff --git a/cla-backend-go/gitlab_api/client_users.go b/cla-backend-go/gitlab_api/client_users.go new file mode 100644 index 000000000..3a3d31df7 --- /dev/null +++ b/cla-backend-go/gitlab_api/client_users.go @@ -0,0 +1,49 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "context" + "errors" + "fmt" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" + goGitLab "github.com/xanzy/go-gitlab" +) + +// GetUserByName gets a gitlab user object by the given name +func GetUserByName(ctx context.Context, client *goGitLab.Client, name string) (*goGitLab.User, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.client_users.GetUserByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "name": name, + } + + users, resp, err := client.Users.ListUsers(&goGitLab.ListUsersOptions{ + ListOptions: goGitLab.ListOptions{ + Page: 0, + PerPage: 10, + }, + Username: utils.StringRef(name), + }) + + if err != nil { + msg := fmt.Sprintf("problem fetching users, error: %+v", err) + log.WithFields(f).WithError(err).Warn(msg) + return nil, errors.New(msg) + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + msg := fmt.Sprintf("unable to get user using query: %s, status code: %d", name, resp.StatusCode) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + + if len(users) == 0 { + return nil, nil + } + + return users[0], nil +} diff --git a/cla-backend-go/gitlab_api/init.go b/cla-backend-go/gitlab_api/init.go new file mode 100644 index 000000000..a4dc16c19 --- /dev/null +++ b/cla-backend-go/gitlab_api/init.go @@ -0,0 +1,52 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "sync" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// App is a wrapper for the GitLab configuration items +type App struct { + gitLabAppID string + gitLabAppSecret string + gitLabAppPrivateKey string +} + +var gitLabAppSingleton *App + +var once sync.Once + +// Init initializes the required gitlab variables +func Init(glAppID, glAppSecret, glAppPrivateKey string) *App { + if gitLabAppSingleton == nil { + once.Do( + func() { + log.Debug("Creating object single instance...") + gitLabAppSingleton = &App{ + gitLabAppID: glAppID, + gitLabAppSecret: glAppSecret, + gitLabAppPrivateKey: glAppPrivateKey, + } + }) + } + return gitLabAppSingleton +} + +// GetAppID returns the GitLab application ID +func (app *App) GetAppID() string { + return app.gitLabAppID +} + +// GetAppSecret returns the GitLab application secret +func (app *App) GetAppSecret() string { + return app.gitLabAppSecret +} + +// GetAppPrivateKey returns the GitLab application private key +func (app *App) GetAppPrivateKey() string { + return app.gitLabAppPrivateKey +} diff --git a/cla-backend-go/gitlab_api/mr.go b/cla-backend-go/gitlab_api/mr.go new file mode 100644 index 000000000..a0db1d259 --- /dev/null +++ b/cla-backend-go/gitlab_api/mr.go @@ -0,0 +1,203 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "fmt" + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" + "github.com/xanzy/go-gitlab" +) + +// FetchMrInfo is responsible for fetching the MR info for given project +func FetchMrInfo(client *gitlab.Client, projectID int, mergeID int) (*gitlab.MergeRequest, error) { + m, _, err := client.MergeRequests.GetMergeRequest(projectID, mergeID, &gitlab.GetMergeRequestsOptions{}) + if err != nil { + return nil, fmt.Errorf("fetching merge request : %d for project : %v failed : %v", mergeID, projectID, err) + } + + return m, nil +} + +func GetLatestCommit(client *gitlab.Client, projectID int, mergeID int) (*gitlab.Commit, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.GetLatestCommit", + "projectID": projectID, + "mergeID": mergeID, + } + + log.WithFields(f).Debug("fetching latest commit...") + commits, _, err := client.MergeRequests.GetMergeRequestCommits(projectID, mergeID, &gitlab.GetMergeRequestCommitsOptions{}) + if err != nil { + return nil, fmt.Errorf("fetching merge request commits : %d for project : %v failed : %v", mergeID, projectID, err) + } + + if len(commits) == 0 { + return nil, fmt.Errorf("no commits found for project : %d and merge id : %d", projectID, mergeID) + } + + return commits[0], nil +} + +// FetchMrParticipants is responsible to get unique mr participants +func FetchMrParticipants(client *gitlab.Client, projectID int, mergeID int) ([]*gitlab.User, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.FetchMrParticipants", + "projectID": projectID, + "mergeID": mergeID, + } + log.WithFields(f).Debug("fetching mr participants...") + commits, response, err := client.MergeRequests.GetMergeRequestCommits(projectID, mergeID, &gitlab.GetMergeRequestCommitsOptions{}) + if err != nil { + return nil, fmt.Errorf("fetching gitlab participants for project : %d and merge id : %d, failed : %v", projectID, mergeID, err) + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("fetching gitlab participants for project : %d and merge id : %d, failed with status code : %d", projectID, mergeID, response.StatusCode) + } + + if len(commits) == 0 { + log.WithFields(f).Debugf("no commits found for project : %d and merge id : %d", projectID, mergeID) + return nil, nil + } + + var results []*gitlab.User + + for _, commit := range commits { + log.WithFields(f).Debugf("commit information: %v", commit) + // The author is the person who originally wrote the code. The committer, on the other hand, is assumed to be + // the person who committed the code on behalf of the original author. + authorEmail := commit.AuthorEmail + authorName := commit.AuthorName + log.WithFields(f).Debugf("extracted authorEmail: %s, user name: %s, from commit: %s. Searching GitLab API...", authorEmail, authorName, commit.ID) + + // attempt to find additional user details - may or may not be able to enrich the user details by adding the GitLab user ID or username + user, getUserErr := getUser(client, &authorEmail, &authorName) + if getUserErr != nil { + log.WithFields(f).Warnf("unable to find user for commit author email : %s, name : %s, error : %v", authorEmail, authorName, getUserErr) + return nil, getUserErr + } + + results = append(results, user) + } + + return results, nil +} + +// SetCommitStatus is responsible for setting the MR status for commit sha +func SetCommitStatus(client *gitlab.Client, projectID int, commitSha string, state gitlab.BuildStateValue, message string, targetURL string) error { + f := logrus.Fields{ + "functionName": "gitlab_api.SetCommitStatus", + "projectID": projectID, + "commitSha": commitSha, + "state": state, + "message": message, + "targetURL": targetURL, + } + + log.WithFields(f).Debug("setting commit status...") + options := &gitlab.SetCommitStatusOptions{ + State: state, + Name: gitlab.String("EasyCLA Bot"), + Description: gitlab.String(message), + } + + if targetURL != "" { + options.TargetURL = gitlab.String(targetURL) + } + + _, _, err := client.Commits.SetCommitStatus(projectID, commitSha, options) + if err != nil { + return fmt.Errorf("setting commit status for the sha : %s and project id : %d failed : %v", commitSha, projectID, err) + } + + log.WithFields(f).Debug("commit status set successfully") + + return nil +} + +// SetMrComment is responsible for setting the comment body for project and merge id +func SetMrComment(client *gitlab.Client, projectID int, mergeID int, message string) error { + + notes, _, err := client.Notes.ListMergeRequestNotes(projectID, mergeID, &gitlab.ListMergeRequestNotesOptions{}) + if err != nil { + return fmt.Errorf("fetching comments for project id : %d and merge id : %d : failed %v", projectID, mergeID, err) + } + + var previousNote *gitlab.Note + + if len(notes) > 0 { + for _, n := range notes { + if strings.Contains(n.Body, "cla-signed.svg") || strings.Contains(n.Body, "cla-not-signed.svg") || strings.Contains(n.Body, "cla-missing-id.svg") || strings.Contains(n.Body, "cla-confirmation-needed.svg") { + previousNote = n + break + } + } + } + + if previousNote == nil { + log.Debugf("creating comment for project id : %d and merge id : %d", projectID, mergeID) + _, _, err = client.Notes.CreateMergeRequestNote(projectID, mergeID, &gitlab.CreateMergeRequestNoteOptions{ + Body: &message, + }) + if err != nil { + return fmt.Errorf("creating comment for project id : %d and merge id : %d : failed %v", projectID, mergeID, err) + } + } else { + log.Debugf("previous comments found for project id : %d and merge id : %d", projectID, mergeID) + _, _, err = client.Notes.UpdateMergeRequestNote(projectID, mergeID, previousNote.ID, &gitlab.UpdateMergeRequestNoteOptions{ + Body: &message, + }) + if err != nil { + return fmt.Errorf("updtae comment for project id : %d and merge id : %d : failed %v", projectID, mergeID, err) + } + } + + return nil +} + +// getUser is responsible for fetching the user info for given user email +func getUser(client *gitlab.Client, email, name *string) (*gitlab.User, error) { + f := logrus.Fields{ + "functionName": "gitlab_api.getUser", + "email": *email, + "name": *name, + } + + user := &gitlab.User{ + Email: *email, + Name: *name, + } + + users, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{ + Active: utils.Bool(true), + Blocked: utils.Bool(false), + Search: email, + }) + if err != nil { + log.WithFields(f).Warnf("unable to find user for email : %s, error : %v", utils.StringValue(email), err) + return nil, err + } + log.WithFields(f).Debugf("found %d users: %+v using email: %s", len(users), users, utils.StringValue(email)) + + if len(users) == 0 { + log.WithFields(f).Warnf("no user found for name : %s", *name) + return user, nil + } + + // check if user exists for the given name + for _, found := range users { + if strings.EqualFold(found.Name, *name) { + log.WithFields(f).Debugf("found matching user : %+v - updating GitLab username and ID", found) + user.Username = found.Username + user.ID = found.ID + break + } + } + + return user, nil +} diff --git a/cla-backend-go/gitlab_api/webhook.go b/cla-backend-go/gitlab_api/webhook.go new file mode 100644 index 000000000..35cc958d2 --- /dev/null +++ b/cla-backend-go/gitlab_api/webhook.go @@ -0,0 +1,83 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "fmt" + + "github.com/xanzy/go-gitlab" +) + +// SetWebHook is responsible for adding the webhook for given projectID, if webhook is there already +// tries to set the attributes if anything is missing, should be idempotent operation +func SetWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int, token string) error { + existingWebHook, err := findExistingWebHook(gitLabClient, hookURL, projectID) + if err != nil { + return err + } + + if existingWebHook == nil { + _, _, err = gitLabClient.Projects.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ + URL: gitlab.String(hookURL), + MergeRequestsEvents: gitlab.Bool(true), + PushEvents: gitlab.Bool(true), + NoteEvents: gitlab.Bool(true), // subscribe to comment events + EnableSSLVerification: gitlab.Bool(true), + Token: gitlab.String(token), + }) + if err != nil { + return fmt.Errorf("adding web hook for project : %d, failed : %v", projectID, err) + } + return nil + } + + if !existingWebHook.EnableSSLVerification || !existingWebHook.MergeRequestsEvents || !existingWebHook.PushEvents { + _, _, err = gitLabClient.Projects.EditProjectHook(projectID, existingWebHook.ID, &gitlab.EditProjectHookOptions{ + URL: gitlab.String(hookURL), + MergeRequestsEvents: gitlab.Bool(true), + PushEvents: gitlab.Bool(true), + NoteEvents: gitlab.Bool(true), // subscribe to comment events + EnableSSLVerification: gitlab.Bool(true), + Token: gitlab.String(token), + }) + if err != nil { + return fmt.Errorf("editing web hook for project : %d, failed : %v", projectID, err) + } + } + + return nil +} + +// RemoveWebHook removes existing webhook from the given project +func RemoveWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int) error { + existingWebHook, err := findExistingWebHook(gitLabClient, hookURL, projectID) + if err != nil { + return err + } + + if existingWebHook == nil { + return nil + } + + _, err = gitLabClient.Projects.DeleteProjectHook(projectID, existingWebHook.ID) + return err + +} + +func findExistingWebHook(gitLabClient *gitlab.Client, hookURL string, projectID int) (*gitlab.ProjectHook, error) { + hooks, _, err := gitLabClient.Projects.ListProjectHooks(projectID, &gitlab.ListProjectHooksOptions{}) + if err != nil { + return nil, fmt.Errorf("fetching hooks for project : %d, failed : %v", projectID, err) + } + + var existingWebHook *gitlab.ProjectHook + for _, hook := range hooks { + if hook.URL == hookURL { + existingWebHook = hook + break + } + } + + return existingWebHook, nil +} diff --git a/cla-backend-go/go.mod b/cla-backend-go/go.mod index 4621ac135..0b70280d6 100644 --- a/cla-backend-go/go.mod +++ b/cla-backend-go/go.mod @@ -2,36 +2,33 @@ // SPDX-License-Identifier: MIT module github.com/communitybridge/easycla/cla-backend-go -go 1.15 +go 1.20 replace github.com/awslabs/aws-lambda-go-api-proxy => github.com/LF-Engineering/aws-lambda-go-api-proxy v0.3.2 require ( github.com/LF-Engineering/aws-lambda-go-api-proxy v0.3.2 - github.com/LF-Engineering/lfx-kit v0.1.22 - github.com/LF-Engineering/lfx-models v0.6.34 + github.com/LF-Engineering/lfx-kit v0.1.33 + github.com/LF-Engineering/lfx-models v0.7.9 github.com/aws/aws-lambda-go v1.22.0 github.com/aws/aws-sdk-go v1.36.27 github.com/aymerick/raymond v2.0.2+incompatible github.com/bitly/go-simplejson v0.5.0 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 - github.com/bradleyfalzon/ghinstallation v1.1.1 github.com/davecgh/go-spew v1.1.1 - github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/fnproject/fdk-go v0.0.2 - github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/go-openapi/errors v0.19.6 - github.com/go-openapi/loads v0.19.5 - github.com/go-openapi/runtime v0.19.19 - github.com/go-openapi/spec v0.19.8 - github.com/go-openapi/strfmt v0.19.5 - github.com/go-openapi/swag v0.19.9 - github.com/go-openapi/validate v0.19.10 + github.com/gin-gonic/gin v1.7.7 + github.com/go-openapi/errors v0.20.2 + github.com/go-openapi/loads v0.21.0 + github.com/go-openapi/runtime v0.21.1 + github.com/go-openapi/spec v0.20.6 + github.com/go-openapi/strfmt v0.21.3 + github.com/go-openapi/swag v0.21.1 + github.com/go-openapi/validate v0.20.3 + github.com/go-playground/validator/v10 v10.7.0 // indirect github.com/go-resty/resty/v2 v2.3.0 - github.com/gofrs/uuid v3.2.0+incompatible - github.com/golang/mock v1.4.4 - github.com/golang/protobuf v1.4.3 // indirect - github.com/google/go-github/v33 v33.0.0 + github.com/gofrs/uuid v4.0.0+incompatible + github.com/golang/mock v1.6.0 + github.com/google/go-github/v37 v37.0.0 github.com/google/uuid v1.1.4 github.com/gorilla/sessions v1.2.1 // indirect github.com/imroc/req v0.3.0 @@ -40,28 +37,91 @@ require ( github.com/jmoiron/sqlx v1.2.0 github.com/juju/mempool v0.0.0-20160205104927-24974d6c264f // indirect github.com/juju/zip v0.0.0-20160205105221-f6b1e93fa2e2 - github.com/kr/pretty v0.2.0 // indirect - github.com/mitchellh/mapstructure v1.3.2 + github.com/kr/pretty v0.3.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/mapstructure v1.5.0 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/mozillazg/request v0.8.0 // indirect github.com/pdfcpu/pdfcpu v0.3.5-0.20200802160406-be1e0eb55afc - github.com/pelletier/go-toml v1.8.0 // indirect github.com/rs/cors v1.7.0 github.com/savaki/dynastore v0.0.0-20171109173440-28d8558bb429 - github.com/sirupsen/logrus v1.7.0 - github.com/spf13/afero v1.3.0 // indirect - github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v1.1.1 - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.6.1 - github.com/tencentyun/scf-go-lib v0.0.0-20200116145541-9a6ea1bf75b8 + github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa + github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.8.4 github.com/verdverm/frisby v0.0.0-20170604211311-b16556248a9a + github.com/xanzy/go-gitlab v0.50.1 go.uber.org/ratelimit v0.1.0 - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect + golang.org/x/net v0.8.0 + golang.org/x/oauth2 v0.6.0 + golang.org/x/sync v0.2.0 + golang.org/x/sys v0.8.0 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e - google.golang.org/appengine v1.6.6 // indirect - google.golang.org/protobuf v1.24.0 // indirect - gopkg.in/ini.v1 v1.57.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect +) + +require ( + github.com/bradleyfalzon/ghinstallation/v2 v2.2.0 + github.com/golang-jwt/jwt/v4 v4.5.0 +) + +require ( + github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310 // indirect + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/cloudflare/circl v1.3.2 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-github/v50 v50.2.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.6.8 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect + github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.6.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/ugorji/go/codec v1.2.6 // indirect + go.mongodb.org/mongo-driver v1.10.1 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/cla-backend-go/go.sum b/cla-backend-go/go.sum index ad6e7314b..512da7ea0 100644 --- a/cla-backend-go/go.sum +++ b/cla-backend-go/go.sum @@ -3,76 +3,101 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Bowery/prompt v0.0.0-20190419144237-972d0ceb96f5/go.mod h1:4/6eNcqZ09BZ9wLK3tZOjBA1nDj+B0728nlX5YRlSmQ= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/LF-Engineering/aws-lambda-go-api-proxy v0.3.2 h1:ZLAgTj9+H3RTmjbRpUamMO8SWS1m4ZKJGGeh9lT985U= github.com/LF-Engineering/aws-lambda-go-api-proxy v0.3.2/go.mod h1:LQj48zwkRwdjVmDCqtPlviW/7IFaSKzz2gDhxRwVrA4= -github.com/LF-Engineering/lfx-kit v0.1.22 h1:4tE1xTvu5CRWIokOo1waOfuB6vgaCpov5glhkdVzbAs= -github.com/LF-Engineering/lfx-kit v0.1.22/go.mod h1:B+pko2SqvGNSG9hWDC35JNZ38nTPt+r5KB6k75xM5vY= -github.com/LF-Engineering/lfx-models v0.6.34 h1:K8al2aTq8nDm3qNmsTNAhZ1uDzfew/UymwbcW9gbDDs= -github.com/LF-Engineering/lfx-models v0.6.34/go.mod h1:AaV7psgE2IPXhaLXYXoFviobYoh09XJ2P/ALOU11OuE= +github.com/LF-Engineering/lfx-kit v0.1.33 h1:UI0vP7zFqolFdF68N0LDB1cKTU1Y16DbqjEiDt9rNKo= +github.com/LF-Engineering/lfx-kit v0.1.33/go.mod h1:e2dnnqQtojsnFX5rmZpWcZNfHirnLrAys0Jak9pdijM= +github.com/LF-Engineering/lfx-models v0.7.9 h1:xuEvRk9b3Nc57i3Hl5mpH3wKG59z38H4rdWd/TXmNmk= +github.com/LF-Engineering/lfx-models v0.7.9/go.mod h1:AaV7psgE2IPXhaLXYXoFviobYoh09XJ2P/ALOU11OuE= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310 h1:dGAdTcqheKrQ/TW76sAcmO2IorwXplUw2inPkOzykbw= +github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v0.0.0-20190129190457-dcf76fe64fb6/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A= github.com/aws/aws-lambda-go v1.22.0 h1:X7BKqIdfoJcbsEIi+Lrt5YjX1HnZexIbNWOQgkYKgfE= github.com/aws/aws-lambda-go v1.22.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.36.27 h1:wc3xLJJHog2SwiqlLnrLUuct/n+dBjB45QhuZw2psVE= github.com/aws/aws-sdk-go v1.36.27/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boltdb/bolt v1.1.0/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I= -github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug= +github.com/bradleyfalzon/ghinstallation/v2 v2.2.0 h1:AVvVU33rE8wdTS1aNnenwpigEBA9mvzI5OhjhZfH/LU= +github.com/bradleyfalzon/ghinstallation/v2 v2.2.0/go.mod h1:xo3iIfK0lDKECe0s19nbxT0KKvk7LsrGc4NxR5ckKMA= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/communitybridge/easycla v1.0.99 h1:PkmkMV7cLH2Q2YNSFiGGmlyrHBXVYdsWMwbXNuMAyqw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk= +github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -80,99 +105,150 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fnproject/fdk-go v0.0.2 h1:nebofQYAY8SbcjqmoaBo6KLNTwUrJq6lGdi7RCbq/EA= -github.com/fnproject/fdk-go v0.0.2/go.mod h1:9m+nEyku9SqJAVJQsfZOZBQzFkCs+jvmbZJhvgDX4ts= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v0.0.0-20180126034611-783c7ee9c14e/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-chi/chi v0.0.0-20180202194135-e223a795a06a/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/analysis v0.19.10 h1:5BHISBAXOc/aJK25irLZnx2D3s6WyYaY9D4gmuz9fdE= github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= +github.com/go-openapi/analysis v0.20.0/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= +github.com/go-openapi/analysis v0.20.1/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.6 h1:xZMThgv5SQ7SMbWtKFkCf9bBdvR2iEyw9k3zGZONuys= github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.1/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= -github.com/go-openapi/loads v0.19.5 h1:jZVYWawIQiA1NBnHla28ktg6hrcfTHsCE+3QLVRBIls= github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4= +github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o= +github.com/go-openapi/loads v0.21.0 h1:jYtUO4wwP7psAweisP/MDoOpdzsYEESdoPcsWjHDR68= +github.com/go-openapi/loads v0.21.0/go.mod h1:rHYve9nZrQ4CJhyeIIFJINGCg1tQpx2yJrrNo8sf1ws= github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/runtime v0.19.15 h1:2GIefxs9Rx1vCDNghRtypRq+ig8KSLrjHbAYI/gCLCM= github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= -github.com/go-openapi/runtime v0.19.19 h1:PCaQSqG0HiCgpekchPrHO9AEc5ZUaAclOUp9T3RSKoQ= -github.com/go-openapi/runtime v0.19.19/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.21.1 h1:/KIG00BzA2x2HRStX2tnhbqbQdPcFlkgsYCiNY20FZs= +github.com/go-openapi/runtime v0.21.1/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg= github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= -github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= -github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNPlDK+Yk= github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI= +github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= +github.com/go-openapi/validate v0.20.3 h1:GZPPhhKSZrE8HjB4eEkoYAZmoWA4+tCemSgINH1/vKw= +github.com/go-openapi/validate v0.20.3/go.mod h1:goDdqVGiigM3jChcrYJxD2joalke3ZXeftD16byIjA4= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.7.0 h1:gLi5ajTBBheLNt0ctewgq7eolXoDALQd5/y90Hh9ZgM= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= -github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= @@ -198,100 +274,122 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/dep v0.5.4/go.mod h1:6RZ2Wai7dSWk7qL55sDYk+8UPFqcW7all2KDBraPPFA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts= -github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= -github.com/google/go-github/v33 v33.0.0 h1:qAf9yP0qc54ufQxzwv+u9H0tiVOnPJxo0lI/JXqw3ZM= -github.com/google/go-github/v33 v33.0.0/go.mod h1:GMdDnVZY/2TsWgp/lkYnpSAh6TrzhANBBwm6k6TTEXg= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3bOQ/tM= +github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4= +github.com/google/go-github/v50 v50.1.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA= +github.com/google/go-github/v50 v50.2.0 h1:j2FyongEHlO9nxXLc+LP3wuBSVU9mVxfpdYUexMpIfk= +github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/google/uuid v0.0.0-20171129191014-dec09d789f3d/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0= github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v0.0.0-20180120075819-c0091a029979/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= +github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ= github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94= github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U= github.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= @@ -304,203 +402,218 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v0.0.0-20180128142709-bca911dae073/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/mempool v0.0.0-20160205104927-24974d6c264f h1:a3Vd00a20dTKLpyS2hdUafNG5zxQdTw5KhDMK5C0a8U= github.com/juju/mempool v0.0.0-20160205104927-24974d6c264f/go.mod h1:+7K7MqWi5xWI+s1LyB2g0Di71jZo27y+XOlmhNtV1Y0= github.com/juju/zip v0.0.0-20160205105221-f6b1e93fa2e2 h1:McU3wXjBrKfJcOt2Pali5qEir9NLrqOh4EECzdWHknM= github.com/juju/zip v0.0.0-20160205105221-f6b1e93fa2e2/go.mod h1:3mJ64RiWU2x9U6IigvcoVLra6LZQTOwMuHpk02OtOJc= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/govendor v1.0.9/go.mod h1:yvmR6q9ZZ7nSF5Wvh40v0wfP+3TwwL8zYQp+itoZSVM= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= -github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0= github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mozillazg/request v0.8.0 h1:TbXeQUdBWr1J1df5Z+lQczDFzX9JD71kTCl7Zu/9rNM= github.com/mozillazg/request v0.8.0/go.mod h1:weoQ/mVFNbWgRBtivCGF1tUT9lwneFesues+CleXMWc= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20180119174237-747514b53ddd h1:b2wg8HW/u55DT7Y/vamdEn/jdvtsGkxzl+0+iHa5YmE= github.com/onsi/ginkgo v0.0.0-20180119174237-747514b53ddd/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.3.0 h1:yPHEatyQC4jN3vdfvqJXG7O9vfC6LhaAV1NEdYpP+h0= github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pdfcpu/pdfcpu v0.3.5-0.20200802160406-be1e0eb55afc h1:JI2yIEkVFpe4eYIM/fTNtlIayTiGj4m+iku5JLx8uOY= github.com/pdfcpu/pdfcpu v0.3.5-0.20200802160406-be1e0eb55afc/go.mod h1:3wwz3xi60q88WM0kKZeOJvdQ4YgW4Og7whEiodseWs8= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= -github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/savaki/dynastore v0.0.0-20171109173440-28d8558bb429 h1:W/FQ2o7cG+X0Wkb8NefNCTRDEodfo6MtfH9BaO8ncMA= github.com/savaki/dynastore v0.0.0-20171109173440-28d8558bb429/go.mod h1:fK0DIsn9VGLYVur3nQ54Yz4LSLLCyDil0gzq5Y8Yzls= github.com/sdboyer/constext v0.0.0-20170321163424-836a14457353/go.mod h1:5HStXbIikwtDAgAIqiQIqVgMn7mlvZa6PTpwiAVYGYg= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA= +github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= +github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= +github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.3.0 h1:Ysnmjh1Di8EaWaBv40CYR4IdaIsBc5996Gh1oZzCBKk= -github.com/spf13/afero v1.3.0/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tencentyun/scf-go-lib v0.0.0-20200116145541-9a6ea1bf75b8 h1:xp/21gmSPTeWIkalsgXw2njIh3zZyrRRcuCgQfOPLLU= -github.com/tencentyun/scf-go-lib v0.0.0-20200116145541-9a6ea1bf75b8/go.mod h1:K3DbqPpP2WE/9MWokWWzgFZcbgtMb9Wd5CYk9AAbEN8= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v0.0.0-20180129160544-d2b24cf3d3b4/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/negroni v0.0.0-20180130044549-22c5532ea862/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/verdverm/frisby v0.0.0-20170604211311-b16556248a9a h1:Mt+KWT4h97wIDQahX1eD3OLkmc/fGbLy7EndiE85kMQ= github.com/verdverm/frisby v0.0.0-20170604211311-b16556248a9a/go.mod h1:Z+jvFzFlZ6eHAKMfi8PZZphUtg4S0gc2EZYOL9UnWgA= +github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ= +github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= -go.mongodb.org/mongo-driver v1.3.4 h1:zs/dKNwX0gYUtzwrN9lLiR15hCO0nDwQj5xXx+vjCdE= go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.10.1 h1:NujsPveKwHaWuKUer/ceo9DzEe7HIj1SlJ6uvXZG0S4= +go.mongodb.org/mongo-driver v1.10.1/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw= go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -509,20 +622,32 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -530,18 +655,26 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -552,18 +685,51 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -571,14 +737,17 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -593,32 +762,79 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -634,25 +850,74 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200424195722-358506031216 h1:tP48fLPdUK6KA51XXU9OBvvjWPzKOwEldOl7Ab4Z+8U= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200424195722-358506031216/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -662,12 +927,49 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -676,35 +978,40 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= -gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/cla-backend-go/health/handlers.go b/cla-backend-go/health/handlers.go index 9fe1f00b3..feba29b42 100644 --- a/cla-backend-go/health/handlers.go +++ b/cla-backend-go/health/handlers.go @@ -4,9 +4,9 @@ package health import ( - "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/health" + "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/health" "github.com/go-openapi/runtime/middleware" ) diff --git a/cla-backend-go/health/service.go b/cla-backend-go/health/service.go index c64f97cba..61177d3a5 100644 --- a/cla-backend-go/health/service.go +++ b/cla-backend-go/health/service.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" log "github.com/communitybridge/easycla/cla-backend-go/logging" - "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" ) diff --git a/cla-backend-go/init/init.go b/cla-backend-go/init/init.go index d0b96041d..f0f4e5635 100644 --- a/cla-backend-go/init/init.go +++ b/cla-backend-go/init/init.go @@ -63,7 +63,7 @@ func GetStage() string { return stage } -// GetConfig returns the configration SSM based on stage, e.g. dev, test, stage or prod +// GetConfig returns the configuration SSM based on stage, e.g. dev, test, stage or prod func GetConfig() config.Config { return configVars } diff --git a/cla-backend-go/package.json b/cla-backend-go/package.json index e854a40bd..07fd70dae 100644 --- a/cla-backend-go/package.json +++ b/cla-backend-go/package.json @@ -1,8 +1,11 @@ { - "name": "easycla-api", + "name": "easycla-go-api", "version": "1.0.0", "license": "MIT", "author": "The Linux Foundation", + "engines": { + "node": ">=18" + }, "scripts": { "sls": "./node_modules/serverless/bin/serverless.js", "deploy:dev": "SLS_DEBUG=* ./node_modules/serverless/bin/serverless.js deploy -s dev -r us-east-2 --verbose", @@ -18,16 +21,32 @@ "dependencies": { "install": "^0.13.0", "node.extend": "^2.0.2", - "request": "^2.88.0", - "serverless": "^2.19.0", - "serverless-finch": "^2.3.2", - "serverless-layers": "^1.4.3", + "serverless": "^3.32.2", + "serverless-finch": "^4.0.3", + "serverless-layers": "^2.6.1", "serverless-plugin-tracing": "^2.0.0", - "serverless-prune-plugin": "^1.4.3", - "serverless-pseudo-parameters": "^2.5.0" + "serverless-prune-plugin": "^2.0.2", + "xml2js": "^0.6.0", + "yarn-audit-fix": "^9.3.10" }, "resolutions": { - "axios": "^0.21.1", - "ini": "^1.3.7" + "axios": "^0.28.0", + "ansi-regex": "^5.0.1", + "aws-sdk": "^2.1329.0", + "cookiejar": "^2.1.4", + "file-type": "^16.5.4", + "follow-redirects": "^1.14.7", + "glob-parent": "^5.1.2", + "http-cache-semantics": "^4.1.1", + "ini": "^1.3.7", + "json-schema": "^0.4.0", + "minimist": "^1.2.6", + "normalize-url": "^4.5.1", + "qs": "^6.11.0", + "set-value": "^4.0.1", + "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-go/project/common/helpers.go b/cla-backend-go/project/common/helpers.go new file mode 100644 index 000000000..20bace463 --- /dev/null +++ b/cla-backend-go/project/common/helpers.go @@ -0,0 +1,143 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "fmt" + "strconv" + "time" + + models2 "github.com/communitybridge/easycla/cla-backend-go/project/models" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "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/sirupsen/logrus" +) + +// AddStringAttribute adds a new string attribute to the existing map +func AddStringAttribute(item map[string]*dynamodb.AttributeValue, key string, value string) { + if value != "" { + item[key] = &dynamodb.AttributeValue{S: aws.String(value)} + } +} + +// AddBooleanAttribute adds a new boolean attribute to the existing map +func AddBooleanAttribute(item map[string]*dynamodb.AttributeValue, key string, value bool) { + item[key] = &dynamodb.AttributeValue{BOOL: aws.Bool(value)} +} + +// AddStringSliceAttribute adds a new string slice attribute to the existing map +func AddStringSliceAttribute(item map[string]*dynamodb.AttributeValue, key string, value []string) { + item[key] = &dynamodb.AttributeValue{SS: aws.StringSlice(value)} +} + +// AddListAttribute adds a list to the existing map +func AddListAttribute(item map[string]*dynamodb.AttributeValue, key string, value []*dynamodb.AttributeValue) { + item[key] = &dynamodb.AttributeValue{L: value} +} + +// BuildCLAGroupDocumentModels builds response models based on the array of db models +func BuildCLAGroupDocumentModels(dbDocumentModels []models2.DBProjectDocumentModel) []models.ClaGroupDocument { + if dbDocumentModels == nil { + return nil + } + + // Response model + var response []models.ClaGroupDocument + + for _, dbDocumentModel := range dbDocumentModels { + response = append(response, models.ClaGroupDocument{ + DocumentName: dbDocumentModel.DocumentName, + DocumentAuthorName: dbDocumentModel.DocumentAuthorName, + DocumentContentType: dbDocumentModel.DocumentContentType, + DocumentFileID: dbDocumentModel.DocumentFileID, + DocumentLegalEntityName: dbDocumentModel.DocumentLegalEntityName, + DocumentPreamble: dbDocumentModel.DocumentPreamble, + DocumentS3URL: dbDocumentModel.DocumentS3URL, + DocumentMajorVersion: dbDocumentModel.DocumentMajorVersion, + DocumentMinorVersion: dbDocumentModel.DocumentMinorVersion, + DocumentCreationDate: dbDocumentModel.DocumentCreationDate, + DocumentTabs: dbDocumentModel.DocumentTabs, + }) + } + + return response +} + +// GetCurrentDocument returns the current document based on the version and date/time +func GetCurrentDocument(ctx context.Context, docs []models.ClaGroupDocument) (models.ClaGroupDocument, error) { + f := logrus.Fields{ + "functionName": "v1.project.helpers.GetCurrentDocument", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + var currentDoc models.ClaGroupDocument + var currentDocVersion float64 + var currentDocDateTime time.Time + for _, doc := range docs { + maj, err := strconv.Atoi(doc.DocumentMajorVersion) + if err != nil { + log.WithFields(f).WithError(err).Warnf("invalid major number in cla group: %s", doc.DocumentMajorVersion) + continue + } + + min, err := strconv.Atoi(doc.DocumentMinorVersion) + if err != nil { + log.WithFields(f).WithError(err).Warnf("invalid minor number in cla group: %s", doc.DocumentMinorVersion) + continue + } + + version, err := strconv.ParseFloat(fmt.Sprintf("%d.%d", maj, min), 32) + if err != nil { + log.WithFields(f).WithError(err).Warnf("invalid major/minor version in cla group: %s.%s", doc.DocumentMajorVersion, doc.DocumentMinorVersion) + continue + } + + dateTime, err := utils.ParseDateTime(doc.DocumentCreationDate) + if err != nil { + log.WithFields(f).WithError(err).Warnf("invalid date time in cla group: %s", doc.DocumentCreationDate) + continue + } + + // // No previous, use the first... + // if currentDoc == (models.ClaGroupDocument{}) { + // currentDoc = doc + // currentDocVersion = version + // currentDocDateTime = dateTime + // continue + // } + + // Newer version... + if version > currentDocVersion { + currentDoc = doc + currentDocVersion = version + currentDocDateTime = dateTime + } + + // Same version, but a later date... + if version == currentDocVersion && dateTime.After(currentDocDateTime) { + currentDoc = doc + currentDocVersion = version + currentDocDateTime = dateTime + } + } + + return currentDoc, nil +} + +func AreClaGroupDocumentsEqual(doc1, doc2 models.ClaGroupDocument) bool { + return doc1.DocumentName == doc2.DocumentName && + doc1.DocumentAuthorName == doc2.DocumentAuthorName && + doc1.DocumentContentType == doc2.DocumentContentType && + doc1.DocumentFileID == doc2.DocumentFileID && + doc1.DocumentLegalEntityName == doc2.DocumentLegalEntityName && + doc1.DocumentPreamble == doc2.DocumentPreamble && + doc1.DocumentS3URL == doc2.DocumentS3URL && + doc1.DocumentMajorVersion == doc2.DocumentMajorVersion && + doc1.DocumentMinorVersion == doc2.DocumentMinorVersion && + doc1.DocumentCreationDate == doc2.DocumentCreationDate +} diff --git a/cla-backend-go/project/errors.go b/cla-backend-go/project/errors.go index 34979b9b8..f62368699 100644 --- a/cla-backend-go/project/errors.go +++ b/cla-backend-go/project/errors.go @@ -4,7 +4,7 @@ package project import ( - "github.com/communitybridge/easycla/cla-backend-go/gen/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" ) // codedResponse interface diff --git a/cla-backend-go/project/handlers.go b/cla-backend-go/project/handlers.go index 74b5321e7..1b6b86b98 100644 --- a/cla-backend-go/project/handlers.go +++ b/cla-backend-go/project/handlers.go @@ -7,6 +7,9 @@ import ( "context" "fmt" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + "github.com/communitybridge/easycla/cla-backend-go/project/service" + "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" @@ -18,9 +21,9 @@ 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/project" + "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/project" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" @@ -35,7 +38,7 @@ func isValidUser(claUser *user.CLAUser) bool { } // Configure establishes the middleware handlers for the project service -func Configure(api *operations.ClaAPI, service Service, eventsService events.Service, gerritService gerrits.Service, repositoryService repositories.Service, signatureService signatures.SignatureService) { +func Configure(api *operations.ClaAPI, service service.Service, eventsService events.Service, gerritService gerrits.Service, repositoryService repositories.Service, signatureService signatures.SignatureService) { // Create CLA Group/Project Handler api.ProjectCreateProjectHandler = project.CreateProjectHandlerFunc(func(params project.CreateProjectParams, claUser *user.CLAUser) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) @@ -76,8 +79,9 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser return project.NewCreateProjectBadRequest().WithPayload(errorResponse(err)) } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CLAGroupCreated, + ProjectSFID: claGroupModel.ProjectExternalID, ClaGroupModel: claGroupModel, UserID: claUser.UserID, LfUsername: claUser.LFUsername, @@ -185,7 +189,7 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser log.WithFields(f).Debug("Processing delete request") claGroupModel, err := service.GetCLAGroupByID(ctx, params.ProjectID) if err != nil { - if err == ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return project.NewDeleteProjectByIDNotFound().WithXRequestID(reqID) } return project.NewDeleteProjectByIDBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) @@ -200,8 +204,9 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser // Log gerrit event if howMany > 0 { log.WithFields(f).Debugf("Deleted %d gerrit groups", howMany) - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.GerritRepositoryDeleted, + ProjectSFID: claGroupModel.ProjectExternalID, ClaGroupModel: claGroupModel, UserID: claUser.UserID, LfUsername: claUser.LFUsername, @@ -221,8 +226,9 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser log.WithFields(f).Debugf("Deleted %d github repositories", howMany) // Log github delete event - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.RepositoryDisabled, + ProjectSFID: claGroupModel.ProjectExternalID, ClaGroupModel: claGroupModel, UserID: claUser.UserID, LfUsername: claUser.LFUsername, @@ -234,15 +240,18 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser // Invalidate project signatures log.WithFields(f).Debug("Invalidating signatures") - howMany, err = signatureService.InvalidateProjectRecords(ctx, params.ProjectID, claGroupModel.ProjectName) + note := fmt.Sprintf("Signature invalidated (approved set to false) by %s due to CLA Group/Project: %s deletion", claUser.LFUsername, params.ProjectID) + + howMany, err = signatureService.InvalidateProjectRecords(ctx, params.ProjectID, note) if err != nil { return project.NewDeleteProjectByIDBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } if howMany > 0 { log.WithFields(f).Debugf("Invalidated %d signatures", howMany) // Log invalidate signatures - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.InvalidatedSignature, + ProjectSFID: claGroupModel.ProjectExternalID, ClaGroupModel: claGroupModel, UserID: claUser.UserID, LfUsername: claUser.LFUsername, @@ -254,13 +263,14 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser err = service.DeleteCLAGroup(ctx, params.ProjectID) if err != nil { - if err == ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return project.NewDeleteProjectByIDNotFound() } return project.NewDeleteProjectByIDBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ EventType: events.CLAGroupDeleted, + ProjectSFID: claGroupModel.ProjectExternalID, ClaGroupModel: claGroupModel, UserID: claUser.UserID, LfUsername: claUser.LFUsername, @@ -275,7 +285,7 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser reqID := utils.GetRequestID(projectParams.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - exitingModel, getErr := service.GetCLAGroupByID(ctx, projectParams.Body.ProjectID) + existingModel, getErr := service.GetCLAGroupByID(ctx, projectParams.Body.ProjectID) if getErr != nil { msg := fmt.Sprintf("Error querying the project by ID, error: %+v", getErr) log.Warnf("Update Project Failed - %s", msg) @@ -286,7 +296,7 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser } // If the project with the same name exists... - if exitingModel == nil { + if existingModel == nil { msg := fmt.Sprintf("unable to locate project with ID: %s", projectParams.Body.ProjectID) log.Warn(msg) return project.NewUpdateProjectNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ @@ -295,23 +305,32 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser }) } + var oldCLAGroupName, oldCLAGroupDescription string + oldCLAGroupName = existingModel.ProjectName + oldCLAGroupDescription = existingModel.ProjectDescription + claGroupModel, err := service.UpdateCLAGroup(ctx, &projectParams.Body) if err != nil { - if err == ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return project.NewUpdateProjectNotFound() } return project.NewUpdateProjectBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err)) } // Log an event - eventsService.LogEvent(&events.LogEventArgs{ + eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + ProjectID: projectParams.Body.ProjectID, + ProjectSFID: projectParams.Body.ProjectExternalID, EventType: events.CLAGroupUpdated, ClaGroupModel: claGroupModel, UserID: claUser.UserID, LfUsername: claUser.LFUsername, EventData: &events.CLAGroupUpdatedEventData{ - ClaGroupName: projectParams.Body.ProjectName, - ClaGroupDescription: projectParams.Body.ProjectDescription, + NewClaGroupName: projectParams.Body.ProjectName, + NewClaGroupDescription: projectParams.Body.ProjectDescription, + + OldClaGroupName: oldCLAGroupName, + OldClaGroupDescription: oldCLAGroupDescription, }, }) diff --git a/cla-backend-go/project/helpers.go b/cla-backend-go/project/helpers.go deleted file mode 100644 index 0d1491860..000000000 --- a/cla-backend-go/project/helpers.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package project - -import ( - "context" - "fmt" - "strconv" - "sync" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/sirupsen/logrus" -) - -// addStringAttribute adds a new string attribute to the existing map -func addStringAttribute(item map[string]*dynamodb.AttributeValue, key string, value string) { - if value != "" { - item[key] = &dynamodb.AttributeValue{S: aws.String(value)} - } -} - -// addBooleanAttribute adds a new boolean attribute to the existing map -func addBooleanAttribute(item map[string]*dynamodb.AttributeValue, key string, value bool) { - item[key] = &dynamodb.AttributeValue{BOOL: aws.Bool(value)} -} - -// addStringSliceAttribute adds a new string slice attribute to the existing map -func addStringSliceAttribute(item map[string]*dynamodb.AttributeValue, key string, value []string) { - item[key] = &dynamodb.AttributeValue{SS: aws.StringSlice(value)} -} - -// addListAttribute adds a list to the existing map -func addListAttribute(item map[string]*dynamodb.AttributeValue, key string, value []*dynamodb.AttributeValue) { - item[key] = &dynamodb.AttributeValue{L: value} -} - -// buildCLAGroupDocumentModels builds response models based on the array of db models -func buildCLAGroupDocumentModels(dbDocumentModels []DBProjectDocumentModel) []models.ClaGroupDocument { - if dbDocumentModels == nil { - return nil - } - - // Response model - var response []models.ClaGroupDocument - - for _, dbDocumentModel := range dbDocumentModels { - response = append(response, models.ClaGroupDocument{ - DocumentName: dbDocumentModel.DocumentName, - DocumentAuthorName: dbDocumentModel.DocumentAuthorName, - DocumentContentType: dbDocumentModel.DocumentContentType, - DocumentFileID: dbDocumentModel.DocumentFileID, - DocumentLegalEntityName: dbDocumentModel.DocumentLegalEntityName, - DocumentPreamble: dbDocumentModel.DocumentPreamble, - DocumentS3URL: dbDocumentModel.DocumentS3URL, - DocumentMajorVersion: dbDocumentModel.DocumentMajorVersion, - DocumentMinorVersion: dbDocumentModel.DocumentMinorVersion, - DocumentCreationDate: dbDocumentModel.DocumentCreationDate, - }) - } - - return response -} - -func (s service) fillRepoInfo(ctx context.Context, project *models.ClaGroup) { - f := logrus.Fields{ - "functionName": "fillRepoInfo", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - } - - var wg sync.WaitGroup - wg.Add(2) - var ghrepos []*models.GithubRepositoriesGroupByOrgs - var gerrits []*models.Gerrit - - go func() { - defer wg.Done() - var err error - ghrepos, err = s.repositoriesRepo.GetCLAGroupRepositoriesGroupByOrgs(ctx, project.ProjectID, true) - if err != nil { - log.WithFields(f).WithError(err).Warnf("unable to get github repositories for cla group ID: %s", project.ProjectID) - return - } - }() - - go func() { - defer wg.Done() - var err error - var gerritsList *models.GerritList - gerritsList, err = s.gerritRepo.GetClaGroupGerrits(ctx, project.ProjectID, nil) - if err != nil { - log.WithFields(f).WithError(err).Warnf("unable to get gerrit instances for cla group ID: %s.", project.ProjectID) - return - } - gerrits = gerritsList.List - }() - - wg.Wait() - project.GithubRepositories = ghrepos - project.Gerrits = gerrits -} - -// GetCurrentDocument returns the current document based on the version and date/time -func GetCurrentDocument(ctx context.Context, docs []models.ClaGroupDocument) (models.ClaGroupDocument, error) { - f := logrus.Fields{ - "functionName": "GetCurrentDocument", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - } - var currentDoc models.ClaGroupDocument - var currentDocVersion float64 - var currentDocDateTime time.Time - for _, doc := range docs { - maj, err := strconv.Atoi(doc.DocumentMajorVersion) - if err != nil { - log.WithFields(f).WithError(err).Warnf("invalid major number in cla group: %s", doc.DocumentMajorVersion) - continue - } - - min, err := strconv.Atoi(doc.DocumentMinorVersion) - if err != nil { - log.WithFields(f).WithError(err).Warnf("invalid minor number in cla group: %s", doc.DocumentMinorVersion) - continue - } - - version, err := strconv.ParseFloat(fmt.Sprintf("%d.%d", maj, min), 32) - if err != nil { - log.WithFields(f).WithError(err).Warnf("invalid major/minor version in cla group: %s.%s", doc.DocumentMajorVersion, doc.DocumentMinorVersion) - continue - } - - dateTime, err := utils.ParseDateTime(doc.DocumentCreationDate) - if err != nil { - log.WithFields(f).WithError(err).Warnf("invalid date time in cla group: %s", doc.DocumentCreationDate) - continue - } - - // No previous, use the first... - if currentDoc == (models.ClaGroupDocument{}) { - currentDoc = doc - currentDocVersion = version - currentDocDateTime = dateTime - continue - } - - // Newer version... - if version > currentDocVersion { - currentDoc = doc - currentDocVersion = version - currentDocDateTime = dateTime - } - - // Same version, but a later date... - if version == currentDocVersion && dateTime.After(currentDocDateTime) { - currentDoc = doc - currentDocVersion = version - currentDocDateTime = dateTime - } - } - - return currentDoc, nil -} diff --git a/cla-backend-go/project/mocks/mock_repo.go b/cla-backend-go/project/mocks/mock_repo.go new file mode 100644 index 000000000..bb3328820 --- /dev/null +++ b/cla-backend-go/project/mocks/mock_repo.go @@ -0,0 +1,203 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: project/repository/repository.go + +// Package mock_repository is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + project "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/project" + gomock "github.com/golang/mock/gomock" +) + +// MockProjectRepository is a mock of ProjectRepository interface. +type MockProjectRepository struct { + ctrl *gomock.Controller + recorder *MockProjectRepositoryMockRecorder +} + +// MockProjectRepositoryMockRecorder is the mock recorder for MockProjectRepository. +type MockProjectRepositoryMockRecorder struct { + mock *MockProjectRepository +} + +// NewMockProjectRepository creates a new mock instance. +func NewMockProjectRepository(ctrl *gomock.Controller) *MockProjectRepository { + mock := &MockProjectRepository{ctrl: ctrl} + mock.recorder = &MockProjectRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProjectRepository) EXPECT() *MockProjectRepositoryMockRecorder { + return m.recorder +} + +// CreateCLAGroup mocks base method. +func (m *MockProjectRepository) CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCLAGroup", ctx, claGroupModel) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCLAGroup indicates an expected call of CreateCLAGroup. +func (mr *MockProjectRepositoryMockRecorder) CreateCLAGroup(ctx, claGroupModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCLAGroup", reflect.TypeOf((*MockProjectRepository)(nil).CreateCLAGroup), ctx, claGroupModel) +} + +// DeleteCLAGroup mocks base method. +func (m *MockProjectRepository) DeleteCLAGroup(ctx context.Context, claGroupID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCLAGroup", ctx, claGroupID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCLAGroup indicates an expected call of DeleteCLAGroup. +func (mr *MockProjectRepositoryMockRecorder) DeleteCLAGroup(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCLAGroup", reflect.TypeOf((*MockProjectRepository)(nil).DeleteCLAGroup), ctx, claGroupID) +} + +// GetCLAGroupByID mocks base method. +func (m *MockProjectRepository) GetCLAGroupByID(ctx context.Context, claGroupID string, loadRepoDetails bool) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupByID", ctx, claGroupID, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupByID indicates an expected call of GetCLAGroupByID. +func (mr *MockProjectRepositoryMockRecorder) GetCLAGroupByID(ctx, claGroupID, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupByID", reflect.TypeOf((*MockProjectRepository)(nil).GetCLAGroupByID), ctx, claGroupID, loadRepoDetails) +} + +// GetCLAGroupByName mocks base method. +func (m *MockProjectRepository) GetCLAGroupByName(ctx context.Context, claGroupName string) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupByName", ctx, claGroupName) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupByName indicates an expected call of GetCLAGroupByName. +func (mr *MockProjectRepositoryMockRecorder) GetCLAGroupByName(ctx, claGroupName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupByName", reflect.TypeOf((*MockProjectRepository)(nil).GetCLAGroupByName), ctx, claGroupName) +} + +// GetCLAGroups mocks base method. +func (m *MockProjectRepository) GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroups", ctx, params) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroups indicates an expected call of GetCLAGroups. +func (mr *MockProjectRepositoryMockRecorder) GetCLAGroups(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroups", reflect.TypeOf((*MockProjectRepository)(nil).GetCLAGroups), ctx, params) +} + +// GetCLAGroupsByExternalID mocks base method. +func (m *MockProjectRepository) GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams, loadRepoDetails bool) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupsByExternalID", ctx, params, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupsByExternalID indicates an expected call of GetCLAGroupsByExternalID. +func (mr *MockProjectRepositoryMockRecorder) GetCLAGroupsByExternalID(ctx, params, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupsByExternalID", reflect.TypeOf((*MockProjectRepository)(nil).GetCLAGroupsByExternalID), ctx, params, loadRepoDetails) +} + +// GetClaGroupByProjectSFID mocks base method. +func (m *MockProjectRepository) GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupByProjectSFID", ctx, projectSFID, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupByProjectSFID indicates an expected call of GetClaGroupByProjectSFID. +func (mr *MockProjectRepositoryMockRecorder) GetClaGroupByProjectSFID(ctx, projectSFID, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupByProjectSFID", reflect.TypeOf((*MockProjectRepository)(nil).GetClaGroupByProjectSFID), ctx, projectSFID, loadRepoDetails) +} + +// GetClaGroupsByFoundationSFID mocks base method. +func (m *MockProjectRepository) GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupsByFoundationSFID", ctx, foundationSFID, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupsByFoundationSFID indicates an expected call of GetClaGroupsByFoundationSFID. +func (mr *MockProjectRepositoryMockRecorder) GetClaGroupsByFoundationSFID(ctx, foundationSFID, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupsByFoundationSFID", reflect.TypeOf((*MockProjectRepository)(nil).GetClaGroupsByFoundationSFID), ctx, foundationSFID, loadRepoDetails) +} + +// GetExternalCLAGroup mocks base method. +func (m *MockProjectRepository) GetExternalCLAGroup(ctx context.Context, claGroupExternalID string) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetExternalCLAGroup", ctx, claGroupExternalID) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetExternalCLAGroup indicates an expected call of GetExternalCLAGroup. +func (mr *MockProjectRepositoryMockRecorder) GetExternalCLAGroup(ctx, claGroupExternalID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExternalCLAGroup", reflect.TypeOf((*MockProjectRepository)(nil).GetExternalCLAGroup), ctx, claGroupExternalID) +} + +// UpdateCLAGroup mocks base method. +func (m *MockProjectRepository) UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCLAGroup", ctx, claGroupModel) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateCLAGroup indicates an expected call of UpdateCLAGroup. +func (mr *MockProjectRepositoryMockRecorder) UpdateCLAGroup(ctx, claGroupModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCLAGroup", reflect.TypeOf((*MockProjectRepository)(nil).UpdateCLAGroup), ctx, claGroupModel) +} + +// UpdateRootCLAGroupRepositoriesCount mocks base method. +func (m *MockProjectRepository) UpdateRootCLAGroupRepositoriesCount(ctx context.Context, claGroupID string, diff int64, reset bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRootCLAGroupRepositoriesCount", ctx, claGroupID, diff, reset) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateRootCLAGroupRepositoriesCount indicates an expected call of UpdateRootCLAGroupRepositoriesCount. +func (mr *MockProjectRepositoryMockRecorder) UpdateRootCLAGroupRepositoriesCount(ctx, claGroupID, diff, reset interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRootCLAGroupRepositoriesCount", reflect.TypeOf((*MockProjectRepository)(nil).UpdateRootCLAGroupRepositoriesCount), ctx, claGroupID, diff, reset) +} diff --git a/cla-backend-go/project/mocks/mock_service.go b/cla-backend-go/project/mocks/mock_service.go new file mode 100644 index 000000000..c8246cdbb --- /dev/null +++ b/cla-backend-go/project/mocks/mock_service.go @@ -0,0 +1,249 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: project/service/service.go + +// Package mock_service is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + project "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/project" + 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 +} + +// CreateCLAGroup mocks base method. +func (m *MockService) CreateCLAGroup(ctx context.Context, project *models.ClaGroup) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCLAGroup", ctx, project) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCLAGroup indicates an expected call of CreateCLAGroup. +func (mr *MockServiceMockRecorder) CreateCLAGroup(ctx, project interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCLAGroup", reflect.TypeOf((*MockService)(nil).CreateCLAGroup), ctx, project) +} + +// DeleteCLAGroup mocks base method. +func (m *MockService) DeleteCLAGroup(ctx context.Context, claGroupID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCLAGroup", ctx, claGroupID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCLAGroup indicates an expected call of DeleteCLAGroup. +func (mr *MockServiceMockRecorder) DeleteCLAGroup(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCLAGroup", reflect.TypeOf((*MockService)(nil).DeleteCLAGroup), ctx, claGroupID) +} + +// GetCLAGroupByID mocks base method. +func (m *MockService) GetCLAGroupByID(ctx context.Context, claGroupID string) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupByID", ctx, claGroupID) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupByID indicates an expected call of GetCLAGroupByID. +func (mr *MockServiceMockRecorder) GetCLAGroupByID(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupByID", reflect.TypeOf((*MockService)(nil).GetCLAGroupByID), ctx, claGroupID) +} + +// GetCLAGroupByName mocks base method. +func (m *MockService) GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupByName", ctx, projectName) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupByName indicates an expected call of GetCLAGroupByName. +func (mr *MockServiceMockRecorder) GetCLAGroupByName(ctx, projectName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupByName", reflect.TypeOf((*MockService)(nil).GetCLAGroupByName), ctx, projectName) +} + +// GetCLAGroupCurrentCCLATemplateURLByID mocks base method. +func (m *MockService) GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupCurrentCCLATemplateURLByID", ctx, claGroupID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupCurrentCCLATemplateURLByID indicates an expected call of GetCLAGroupCurrentCCLATemplateURLByID. +func (mr *MockServiceMockRecorder) GetCLAGroupCurrentCCLATemplateURLByID(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupCurrentCCLATemplateURLByID", reflect.TypeOf((*MockService)(nil).GetCLAGroupCurrentCCLATemplateURLByID), ctx, claGroupID) +} + +// GetCLAGroupCurrentICLATemplateURLByID mocks base method. +func (m *MockService) GetCLAGroupCurrentICLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupCurrentICLATemplateURLByID", ctx, claGroupID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupCurrentICLATemplateURLByID indicates an expected call of GetCLAGroupCurrentICLATemplateURLByID. +func (mr *MockServiceMockRecorder) GetCLAGroupCurrentICLATemplateURLByID(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupCurrentICLATemplateURLByID", reflect.TypeOf((*MockService)(nil).GetCLAGroupCurrentICLATemplateURLByID), ctx, claGroupID) +} + +// GetCLAGroups mocks base method. +func (m *MockService) GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroups", ctx, params) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroups indicates an expected call of GetCLAGroups. +func (mr *MockServiceMockRecorder) GetCLAGroups(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroups", reflect.TypeOf((*MockService)(nil).GetCLAGroups), ctx, params) +} + +// GetCLAGroupsByExternalID mocks base method. +func (m *MockService) GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupsByExternalID", ctx, params) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupsByExternalID indicates an expected call of GetCLAGroupsByExternalID. +func (mr *MockServiceMockRecorder) GetCLAGroupsByExternalID(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupsByExternalID", reflect.TypeOf((*MockService)(nil).GetCLAGroupsByExternalID), ctx, params) +} + +// GetCLAGroupsByExternalSFID mocks base method. +func (m *MockService) GetCLAGroupsByExternalSFID(ctx context.Context, projectSFID string) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupsByExternalSFID", ctx, projectSFID) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupsByExternalSFID indicates an expected call of GetCLAGroupsByExternalSFID. +func (mr *MockServiceMockRecorder) GetCLAGroupsByExternalSFID(ctx, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupsByExternalSFID", reflect.TypeOf((*MockService)(nil).GetCLAGroupsByExternalSFID), ctx, projectSFID) +} + +// GetCLAManagers mocks base method. +func (m *MockService) GetCLAManagers(ctx context.Context, claGroupID string) ([]*models.ClaManagerUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAManagers", ctx, claGroupID) + ret0, _ := ret[0].([]*models.ClaManagerUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAManagers indicates an expected call of GetCLAManagers. +func (mr *MockServiceMockRecorder) GetCLAManagers(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAManagers", reflect.TypeOf((*MockService)(nil).GetCLAManagers), ctx, claGroupID) +} + +// GetClaGroupByProjectSFID mocks base method. +func (m *MockService) GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupByProjectSFID", ctx, projectSFID, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupByProjectSFID indicates an expected call of GetClaGroupByProjectSFID. +func (mr *MockServiceMockRecorder) GetClaGroupByProjectSFID(ctx, projectSFID, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupByProjectSFID", reflect.TypeOf((*MockService)(nil).GetClaGroupByProjectSFID), ctx, projectSFID, loadRepoDetails) +} + +// GetClaGroupsByFoundationSFID mocks base method. +func (m *MockService) GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupsByFoundationSFID", ctx, foundationSFID, loadRepoDetails) + ret0, _ := ret[0].(*models.ClaGroups) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupsByFoundationSFID indicates an expected call of GetClaGroupsByFoundationSFID. +func (mr *MockServiceMockRecorder) GetClaGroupsByFoundationSFID(ctx, foundationSFID, loadRepoDetails interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupsByFoundationSFID", reflect.TypeOf((*MockService)(nil).GetClaGroupsByFoundationSFID), ctx, foundationSFID, loadRepoDetails) +} + +// SignedAtFoundationLevel mocks base method. +func (m *MockService) SignedAtFoundationLevel(ctx context.Context, foundationSFID string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignedAtFoundationLevel", ctx, foundationSFID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignedAtFoundationLevel indicates an expected call of SignedAtFoundationLevel. +func (mr *MockServiceMockRecorder) SignedAtFoundationLevel(ctx, foundationSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignedAtFoundationLevel", reflect.TypeOf((*MockService)(nil).SignedAtFoundationLevel), ctx, foundationSFID) +} + +// UpdateCLAGroup mocks base method. +func (m *MockService) UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCLAGroup", ctx, claGroupModel) + ret0, _ := ret[0].(*models.ClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateCLAGroup indicates an expected call of UpdateCLAGroup. +func (mr *MockServiceMockRecorder) UpdateCLAGroup(ctx, claGroupModel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCLAGroup", reflect.TypeOf((*MockService)(nil).UpdateCLAGroup), ctx, claGroupModel) +} diff --git a/cla-backend-go/project/models.go b/cla-backend-go/project/models.go deleted file mode 100644 index 4d9ea93ac..000000000 --- a/cla-backend-go/project/models.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package project - -// DBProjectModel data model -type DBProjectModel struct { - DateCreated string `dynamodbav:"date_created"` - DateModified string `dynamodbav:"date_modified"` - ProjectExternalID string `dynamodbav:"project_external_id"` - ProjectID string `dynamodbav:"project_id"` - FoundationSFID string `dynamodbav:"foundation_sfid"` - RootProjectRepositoriesCount int64 `dynamodbav:"root_project_repositories_count"` - ProjectName string `dynamodbav:"project_name"` - ProjectNameLower string `dynamodbav:"project_name_lower"` - ProjectDescription string `dynamodbav:"project_description"` - Version string `dynamodbav:"version"` - ProjectCclaEnabled bool `dynamodbav:"project_ccla_enabled"` - ProjectCclaRequiresIclaSignature bool `dynamodbav:"project_ccla_requires_icla_signature"` - ProjectIclaEnabled bool `dynamodbav:"project_icla_enabled"` - ProjectLive bool `dynamodbav:"project_live"` - ProjectCorporateDocuments []DBProjectDocumentModel `dynamodbav:"project_corporate_documents"` - ProjectIndividualDocuments []DBProjectDocumentModel `dynamodbav:"project_individual_documents"` - ProjectMemberDocuments []DBProjectDocumentModel `dynamodbav:"project_member_documents"` - ProjectACL []string `dynamodbav:"project_acl"` -} - -// DBProjectDocumentModel is a data model for the CLA Group Project documents -type DBProjectDocumentModel struct { - DocumentName string `dynamodbav:"document_name"` - DocumentFileID string `dynamodbav:"document_file_id"` - DocumentPreamble string `dynamodbav:"document_preamble"` - DocumentLegalEntityName string `dynamodbav:"document_legal_entity_name"` - DocumentAuthorName string `dynamodbav:"document_author_name"` - DocumentContentType string `dynamodbav:"document_content_type"` - DocumentS3URL string `dynamodbav:"document_s3_url"` - DocumentMajorVersion string `dynamodbav:"document_major_version"` - DocumentMinorVersion string `dynamodbav:"document_minor_version"` - DocumentCreationDate string `dynamodbav:"document_creation_date"` -} diff --git a/cla-backend-go/project/models/models.go b/cla-backend-go/project/models/models.go new file mode 100644 index 000000000..cf60318f2 --- /dev/null +++ b/cla-backend-go/project/models/models.go @@ -0,0 +1,46 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package models + +import ( + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" +) + +// DBProjectModel data model +type DBProjectModel struct { + DateCreated string `dynamodbav:"date_created"` + DateModified string `dynamodbav:"date_modified"` + ProjectExternalID string `dynamodbav:"project_external_id"` + ProjectID string `dynamodbav:"project_id"` + FoundationSFID string `dynamodbav:"foundation_sfid"` + RootProjectRepositoriesCount int64 `dynamodbav:"root_project_repositories_count"` + ProjectName string `dynamodbav:"project_name"` + ProjectNameLower string `dynamodbav:"project_name_lower"` + ProjectDescription string `dynamodbav:"project_description"` + Version string `dynamodbav:"version"` + ProjectTemplateID string `dynamodbav:"project_template_id"` + ProjectCclaEnabled bool `dynamodbav:"project_ccla_enabled"` + ProjectCclaRequiresIclaSignature bool `dynamodbav:"project_ccla_requires_icla_signature"` + ProjectIclaEnabled bool `dynamodbav:"project_icla_enabled"` + ProjectLive bool `dynamodbav:"project_live"` + ProjectCorporateDocuments []DBProjectDocumentModel `dynamodbav:"project_corporate_documents"` + ProjectIndividualDocuments []DBProjectDocumentModel `dynamodbav:"project_individual_documents"` + ProjectMemberDocuments []DBProjectDocumentModel `dynamodbav:"project_member_documents"` + ProjectACL []string `dynamodbav:"project_acl"` +} + +// DBProjectDocumentModel is a data model for the CLA Group Project documents +type DBProjectDocumentModel struct { + DocumentName string `dynamodbav:"document_name"` + DocumentFileID string `dynamodbav:"document_file_id"` + DocumentPreamble string `dynamodbav:"document_preamble"` + DocumentLegalEntityName string `dynamodbav:"document_legal_entity_name"` + DocumentAuthorName string `dynamodbav:"document_author_name"` + DocumentContentType string `dynamodbav:"document_content_type"` + DocumentS3URL string `dynamodbav:"document_s3_url"` + DocumentMajorVersion string `dynamodbav:"document_major_version"` + DocumentMinorVersion string `dynamodbav:"document_minor_version"` + DocumentCreationDate string `dynamodbav:"document_creation_date"` + DocumentTabs []v1Models.DocumentTab `dynamodbav:"document_tabs"` +} diff --git a/cla-backend-go/project/repository.go b/cla-backend-go/project/repository.go deleted file mode 100644 index 9553c31be..000000000 --- a/cla-backend-go/project/repository.go +++ /dev/null @@ -1,900 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package project - -import ( - "context" - "errors" - "fmt" - "strconv" - "strings" - "sync" - - "github.com/sirupsen/logrus" - - "github.com/communitybridge/easycla/cla-backend-go/gerrits" - "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/gen/restapi/operations/project" - - "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/gofrs/uuid" - - "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" - "github.com/communitybridge/easycla/cla-backend-go/gen/models" - log "github.com/communitybridge/easycla/cla-backend-go/logging" -) - -// errors -var ( - ErrProjectDoesNotExist = errors.New("project does not exist") - ErrProjectIDMissing = errors.New("project id is missing") -) - -// constants -const ( - LoadRepoDetails = true - DontLoadRepoDetails = false -) - -// ProjectRepository defines functions of Project repository -type ProjectRepository interface { //nolint - CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) - GetCLAGroupByID(ctx context.Context, claGroupID string, loadRepoDetails bool) (*models.ClaGroup, error) - GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams, loadRepoDetails bool) (*models.ClaGroups, error) - GetCLAGroupByName(ctx context.Context, claGroupName string) (*models.ClaGroup, error) - GetExternalCLAGroup(ctx context.Context, claGroupExternalID string) (*models.ClaGroup, error) - GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) - DeleteCLAGroup(ctx context.Context, claGroupID string) error - UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) - - GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) - GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) - UpdateRootCLAGroupRepositoriesCount(ctx context.Context, claGroupID string, diff int64, reset bool) error -} - -// NewRepository creates instance of project repository -func NewRepository(awsSession *session.Session, stage string, ghRepo repositories.Repository, gerritRepo gerrits.Repository, projectClaGroupRepo projects_cla_groups.Repository) ProjectRepository { - return &repo{ - dynamoDBClient: dynamodb.New(awsSession), - stage: stage, - ghRepo: ghRepo, - gerritRepo: gerritRepo, - projectClaGroupRepo: projectClaGroupRepo, - claGroupTable: fmt.Sprintf("cla-%s-projects", stage), - } -} - -type repo struct { - stage string - dynamoDBClient *dynamodb.DynamoDB - ghRepo repositories.Repository - gerritRepo gerrits.Repository - projectClaGroupRepo projects_cla_groups.Repository - claGroupTable string -} - -// CreateCLAGroup creates a new CLA Group -func (repo *repo) CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "CreateCLAGroup", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectName": claGroupModel.ProjectName, - "projectExternalID": claGroupModel.ProjectExternalID, - "foundationSFID": claGroupModel.FoundationSFID, - "tableName": repo.claGroupTable, - } - // Generate a new CLA Group ID - claGroupID, err := uuid.NewV4() - if err != nil { - log.WithFields(f).Warnf("Unable to generate a UUID for a new CLA Group request, error: %v", err) - return nil, err - } - f["claGroupID"] = claGroupID - - _, currentTimeString := utils.CurrentTime() - input := &dynamodb.PutItemInput{ - Item: map[string]*dynamodb.AttributeValue{}, - TableName: aws.String(repo.claGroupTable), - } - - //var individualDocs []*dynamodb.AttributeValue - //var corporateDocs []*dynamodb.AttributeValue - addStringAttribute(input.Item, "project_id", claGroupID.String()) - addStringAttribute(input.Item, "project_external_id", claGroupModel.ProjectExternalID) - addStringAttribute(input.Item, "foundation_sfid", claGroupModel.FoundationSFID) - addStringAttribute(input.Item, "project_description", claGroupModel.ProjectDescription) - addStringAttribute(input.Item, "project_name", claGroupModel.ProjectName) - addStringAttribute(input.Item, "project_name_lower", strings.ToLower(claGroupModel.ProjectName)) - addStringSliceAttribute(input.Item, "project_acl", claGroupModel.ProjectACL) - addBooleanAttribute(input.Item, "project_icla_enabled", claGroupModel.ProjectICLAEnabled) - addBooleanAttribute(input.Item, "project_ccla_enabled", claGroupModel.ProjectCCLAEnabled) - addBooleanAttribute(input.Item, "project_ccla_requires_icla_signature", claGroupModel.ProjectCCLARequiresICLA) - addBooleanAttribute(input.Item, "project_live", claGroupModel.ProjectLive) - - // Empty documents for now - will add the template details later - addListAttribute(input.Item, "project_corporate_documents", []*dynamodb.AttributeValue{}) - addListAttribute(input.Item, "project_individual_documents", []*dynamodb.AttributeValue{}) - addListAttribute(input.Item, "project_member_documents", []*dynamodb.AttributeValue{}) - - addStringAttribute(input.Item, "date_created", currentTimeString) - addStringAttribute(input.Item, "date_modified", currentTimeString) - // Set the version attribute if not already set - if claGroupModel.Version == "" { - claGroupModel.Version = utils.V1 // default value - } - addStringAttribute(input.Item, "version", claGroupModel.Version) - - _, err = repo.dynamoDBClient.PutItem(input) - if err != nil { - log.WithFields(f).Warnf("Unable to create a new CLA Group record, error: %v", err) - return nil, err - } - - // Re-use the provided model - just update the dynamically assigned values - claGroupModel.ProjectID = claGroupID.String() - claGroupModel.DateCreated = currentTimeString - claGroupModel.DateModified = currentTimeString - - return claGroupModel, nil -} - -func (repo *repo) getCLAGroupByID(ctx context.Context, claGroupID string, loadCLAGroupDetails bool) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "getCLAGroupByID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "loadProjectDetails": loadCLAGroupDetails, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("loading cla group...") - // This is the key we want to match - condition := expression.Key("project_id").Equal(expression.Value(claGroupID)) - - // Use the builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for CLA Group query, claGroupID: %s, error: %v", - claGroupID, err) - return nil, err - } - - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - KeyConditionExpression: expr.KeyCondition(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.claGroupTable), - } - - // Make the DynamoDB Query API call - results, queryErr := repo.dynamoDBClient.Query(queryInput) - if queryErr != nil { - log.WithFields(f).Warnf("error retrieving cla group by claGroupID: %s, error: %v", claGroupID, queryErr) - return nil, queryErr - } - - if len(results.Items) < 1 { - return nil, &utils.CLAGroupNotFound{CLAGroupID: claGroupID} - } - var dbModel DBProjectModel - err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbModel) - if err != nil { - log.WithFields(f).Warnf("error unmarshalling db cla group model, error: %+v", err) - return nil, err - } - - // Convert the database model to an API response model - return repo.buildCLAGroupModel(ctx, dbModel, loadCLAGroupDetails), nil -} - -// GetCLAGroupByID returns the cla group model associated for the specified claGroupID -func (repo *repo) GetCLAGroupByID(ctx context.Context, claGroupID string, loadRepoDetails bool) (*models.ClaGroup, error) { - return repo.getCLAGroupByID(ctx, claGroupID, loadRepoDetails) -} - -// GetCLAGroupsByExternalID queries the database and returns a list of the cla groups -func (repo *repo) GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams, loadRepoDetails bool) (*models.ClaGroups, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroupsByExternalID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "ProjectSFID": params.ProjectSFID, - "NextKey": params.NextKey, - "PageSize": params.PageSize, - "loadRepoDetails": loadRepoDetails, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("loading cla group") - - // This is the key we want to match - condition := expression.Key("project_external_id").Equal(expression.Value(params.ProjectSFID)) - - // Use the nice builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for cla group query, error: %v", err) - } - - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - KeyConditionExpression: expr.KeyCondition(), - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.claGroupTable), - IndexName: aws.String("external-project-index"), - } - - // If we have the next key, set the exclusive start key value - if params.NextKey != nil && *params.NextKey != "" { - log.WithFields(f).Debugf("Received a nextKey, value: %s", *params.NextKey) - // The primary key of the first item that this operation will evaluate. - // and the query key (if not the same) - queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "project_id": { - S: params.NextKey, - }, - } - } - - var pageSize *int64 - // If we have a page size, set the limit value - make sure it's a positive value - if params.PageSize != nil && *params.PageSize > 0 { - log.WithFields(f).Debugf("Received a pageSize parameter, value: %d", *params.PageSize) - pageSize = params.PageSize - } else { - // Default page size - pageSize = aws.Int64(50) - } - queryInput.Limit = pageSize - - var projects []models.ClaGroup - var lastEvaluatedKey string - - // Loop until we have all the records - for ok := true; ok; ok = lastEvaluatedKey != "" { - results, errQuery := repo.dynamoDBClient.Query(queryInput) - if errQuery != nil { - log.WithFields(f).Warnf("error retrieving projects, error: %v", errQuery) - return nil, errQuery - } - - // Convert the list of DB models to a list of response models - projectList, modelErr := repo.buildCLAGroupModels(ctx, results.Items, loadRepoDetails) - if modelErr != nil { - log.WithFields(f).Warnf("error converting project DB model to response model, error: %v", - modelErr) - return nil, modelErr - } - - // Add to the project response models to the list - projects = append(projects, projectList...) - - if results.LastEvaluatedKey["project_id"] != nil { - lastEvaluatedKey = *results.LastEvaluatedKey["project_id"].S - queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "project_id": { - S: aws.String(lastEvaluatedKey), - }, - } - } else { - lastEvaluatedKey = "" - } - - if int64(len(projects)) >= *pageSize { - break - } - } - - return &models.ClaGroups{ - LastKeyScanned: lastEvaluatedKey, - PageSize: *pageSize, - ResultCount: int64(len(projects)), - Projects: projects, - }, nil -} - -// GetClaGroupsByFoundationID queries the database and returns a list of all cla_groups associated with foundation -func (repo *repo) GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) { - f := logrus.Fields{ - "functionName": "GetClaGroupsByFoundationSFID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "foundationSFID": foundationSFID, - "loadRepoDetails": loadRepoDetails, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("loading project by foundation SFID") - - // This is the key we want to match - condition := expression.Key("foundation_sfid").Equal(expression.Value(foundationSFID)) - - // Use the nice builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for project scan, error: %v", err) - } - - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - KeyConditionExpression: expr.KeyCondition(), - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.claGroupTable), - IndexName: aws.String("foundation-sfid-project-name-index"), - } - - var projects []models.ClaGroup - for { - results, errQuery := repo.dynamoDBClient.Query(queryInput) - if errQuery != nil { - log.WithFields(f).Warnf("error retrieving projects, error: %v", errQuery) - return nil, errQuery - } - - // Convert the list of DB models to a list of response models - projectList, modelErr := repo.buildCLAGroupModels(ctx, results.Items, loadRepoDetails) - if modelErr != nil { - log.WithFields(f).Warnf("error converting project DB model to response model, error: %v", - modelErr) - return nil, modelErr - } - - // Add to the project response models to the list - projects = append(projects, projectList...) - - if len(results.LastEvaluatedKey) != 0 { - queryInput.ExclusiveStartKey = results.LastEvaluatedKey - } else { - break - } - } - - return &models.ClaGroups{ - ResultCount: int64(len(projects)), - Projects: projects, - }, nil -} - -// GetClaGroupsByProjectSFID returns cla_group associated with project -func (repo *repo) GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "GetClaGroupByProjectSFID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectSFID": projectSFID, - "loadRepoDetails": loadRepoDetails, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("loading project") - - claGroupProject, err := repo.projectClaGroupRepo.GetClaGroupIDForProject(projectSFID) - if err != nil { - log.WithFields(f).Warnf("error fetching CLA Group ID for project, error: %v", err) - return nil, err - } - - return repo.getCLAGroupByID(ctx, claGroupProject.ClaGroupID, loadRepoDetails) -} - -// GetCLAGroupByName returns the project model associated for the specified project name -func (repo *repo) GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroupByName", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectName": projectName, - "tableName": repo.claGroupTable, - } - log.WithFields(f).Debugf("loading project") - - // This is the key we want to match - condition := expression.Key("project_name_lower").Equal(expression.Value(strings.ToLower(projectName))) - - // Use the builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() - if err != nil { - log.WithFields(f).WithError(err).Warnf("error building expression for CLAGroup query, projectName: %s", projectName) - return nil, err - } - - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - KeyConditionExpression: expr.KeyCondition(), - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.claGroupTable), - IndexName: aws.String("project-name-lower-search-index"), - } - - // Make the DynamoDB Query API call - results, queryErr := repo.dynamoDBClient.Query(queryInput) - if queryErr != nil { - log.WithFields(f).Warnf("error retrieving project by projectName: %s, error: %v", projectName, queryErr) - return nil, queryErr - } - - // Should only have one result - if *results.Count > 1 { - log.WithFields(f).Warnf("CLAGroup scan by name returned more than one result using projectName: %s", projectName) - } - - // Didn't find it... - if *results.Count == 0 { - log.WithFields(f).Debugf("CLAGroup scan by name returned no results using projectName: %s", projectName) - return nil, nil - } - - // Found it... - var dbModel DBProjectModel - err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbModel) - if err != nil { - log.WithFields(f).Warnf("error unmarshalling db project model, error: %+v", err) - return nil, err - } - - // Convert the database model to an API response model - return repo.buildCLAGroupModel(ctx, dbModel, LoadRepoDetails), nil -} - -// GetExternalCLAGroup returns the project model associated for the specified external project ID -func (repo *repo) GetExternalCLAGroup(ctx context.Context, projectExternalID string) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "GetExternalCLAGroup", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectExternalID": projectExternalID, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("loading project") - // This is the key we want to match - condition := expression.Key("project_external_id").Equal(expression.Value(projectExternalID)) - - // Use the builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for CLAGroup query, projectExternalID: %s, error: %v", - projectExternalID, err) - return nil, err - } - - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - KeyConditionExpression: expr.KeyCondition(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.claGroupTable), - IndexName: aws.String("external-project-index"), - } - - // Make the DynamoDB Query API call - results, queryErr := repo.dynamoDBClient.Query(queryInput) - if queryErr != nil { - log.WithFields(f).Warnf("error retrieving project by projectExternalID: %s, error: %v", projectExternalID, queryErr) - return nil, queryErr - } - - // No match, didn't find it - if *results.Count == 0 { - return nil, nil - } - - // Should only have one result - if *results.Count > 1 { - log.WithFields(f).Warnf("CLAGroup query returned more than one result using projectExternalID: %s", projectExternalID) - } - - var dbModel DBProjectModel - err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbModel) - if err != nil { - log.WithFields(f).Warnf("error unmarshalling db project model, error: %+v", err) - return nil, err - } - - // Convert the database model to an API response model - return repo.buildCLAGroupModel(ctx, dbModel, LoadRepoDetails), nil -} - -// GetCLAGroups queries the database and returns a list of the projects -func (repo *repo) GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroups", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "searchField": params.SearchField, - "searchTerm": params.SearchTerm, - "nextKey": params.NextKey, - "pageSize": params.PageSize, - "fullMatch": params.FullMatch, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("searching project") - - // Use the nice builder to create the expression - expr, err := expression.NewBuilder().WithProjection(buildProjection()).Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for project scan, error: %v", err) - } - - // Assemble the query input parameters - scanInput := &dynamodb.ScanInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - FilterExpression: expr.Filter(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.claGroupTable), - } - - // If we have the next key, set the exclusive start key value - if params.NextKey != nil && *params.NextKey != "" { - log.WithFields(f).Debugf("Received a nextKey, value: %s", *params.NextKey) - // The primary key of the first item that this operation will evaluate. - // and the query key (if not the same) - scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "project_id": { - S: params.NextKey, - }, - } - } - var pageSize int64 - // If we have a page size, set the limit value - make sure it's a positive value - if params.PageSize != nil && *params.PageSize > 0 { - log.WithFields(f).Debugf("Received a pageSize parameter, value: %d", *params.PageSize) - // The primary key of the first item that this operation will evaluate. - // and the query key (if not the same) - scanInput.Limit = params.PageSize - } else { - // Default page size - pageSize = 50 - params.PageSize = &pageSize - } - - var projects []models.ClaGroup - var lastEvaluatedKey string - - // Loop until we have all the records - for ok := true; ok; ok = lastEvaluatedKey != "" { - results, errQuery := repo.dynamoDBClient.Scan(scanInput) - if errQuery != nil { - log.WithFields(f).Warnf("error retrieving projects, error: %v", errQuery) - return nil, errQuery - } - - // Convert the list of DB models to a list of response models - projectList, modelErr := repo.buildCLAGroupModels(ctx, results.Items, LoadRepoDetails) - if modelErr != nil { - log.WithFields(f).Warnf("error converting project DB model to response model, error: %v", - modelErr) - return nil, modelErr - } - - // Add to the project response models to the list - projects = append(projects, projectList...) - - if results.LastEvaluatedKey["project_id"] != nil { - lastEvaluatedKey = *results.LastEvaluatedKey["project_id"].S - scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "project_id": { - S: aws.String(lastEvaluatedKey), - }, - } - } else { - lastEvaluatedKey = "" - } - - if int64(len(projects)) >= *params.PageSize { - break - } - } - - return &models.ClaGroups{ - LastKeyScanned: lastEvaluatedKey, - PageSize: *params.PageSize, - Projects: projects, - }, nil -} - -// DeleteCLAGroup deletes the CLAGroup by claGroupID -func (repo *repo) DeleteCLAGroup(ctx context.Context, claGroupID string) error { - f := logrus.Fields{ - "functionName": "DeleteCLAGroup", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("deleting CLA Group") - - existingCLAGroup, getErr := repo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) - if getErr != nil { - log.WithFields(f).Warnf("delete - error locating the CLA Group, error: %+v", getErr) - return getErr - } - - if existingCLAGroup == nil { - log.WithFields(f).Warn("unable to locate CLA Group by ID - CLA Group does not exist") - return ErrProjectDoesNotExist - } - - var deleteErr error - // Perform the delete - _, deleteErr = repo.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ - TableName: aws.String(repo.claGroupTable), - Key: map[string]*dynamodb.AttributeValue{ - "project_id": { - S: aws.String(existingCLAGroup.ProjectID), - }, - }, - }) - - if deleteErr != nil { - log.WithFields(f).Warnf("Error deleting project with CLA Group ID : %s, error: %v", claGroupID, deleteErr) - return deleteErr - } - - return nil -} - -// UpdateCLAGroup updates the project by claGroupID -func (repo *repo) UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "UpdateCLAGroup", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "ProjectID": claGroupModel.ProjectID, - "ProjectName": claGroupModel.ProjectName, - "FoundationSFID": claGroupModel.FoundationSFID, - "ProjectExternalID": claGroupModel.ProjectExternalID, - "ProjectICLAEnabled": claGroupModel.ProjectICLAEnabled, - "ProjectCCLAEnabled": claGroupModel.ProjectCCLAEnabled, - "ProjectCCLARequiresICLA": claGroupModel.ProjectCCLARequiresICLA, - "ProjectLive": claGroupModel.ProjectLive, - "tableName": repo.claGroupTable} - log.WithFields(f).Debugf("processing update CLA Group request") - - if claGroupModel.ProjectID == "" { - return nil, ErrProjectIDMissing - } - - existingCLAGroup, getErr := repo.GetCLAGroupByID(ctx, claGroupModel.ProjectID, DontLoadRepoDetails) - if getErr != nil { - log.WithFields(f).Warnf("update - error locating the project id: %s, error: %+v", claGroupModel.ProjectID, getErr) - return nil, getErr - } - - if existingCLAGroup == nil { - return nil, ErrProjectDoesNotExist - } - - expressionAttributeNames := map[string]*string{} - expressionAttributeValues := map[string]*dynamodb.AttributeValue{} - updateExpression := "SET " - - if claGroupModel.ProjectName != "" && existingCLAGroup.ProjectName != claGroupModel.ProjectName { - log.WithFields(f).Debugf("adding project_name: %s", claGroupModel.ProjectName) - expressionAttributeNames["#N"] = aws.String("project_name") - expressionAttributeValues[":n"] = &dynamodb.AttributeValue{S: aws.String(claGroupModel.ProjectName)} - updateExpression = updateExpression + " #N = :n, " - log.WithFields(f).Debugf("adding project name lower: %s", strings.ToLower(claGroupModel.ProjectName)) - expressionAttributeNames["#LOW"] = aws.String("project_name_lower") - expressionAttributeValues[":low"] = &dynamodb.AttributeValue{S: aws.String(strings.ToLower(claGroupModel.ProjectName))} - updateExpression = updateExpression + " #LOW = :low, " - } - - if existingCLAGroup.ProjectDescription != claGroupModel.ProjectDescription { - log.WithFields(f).Debugf("adding project_description: %s", claGroupModel.ProjectDescription) - expressionAttributeNames["#DESC"] = aws.String("project_description") - expressionAttributeValues[":desc"] = &dynamodb.AttributeValue{S: aws.String(claGroupModel.ProjectDescription)} - updateExpression = updateExpression + " #DESC = :desc, " - } - - if claGroupModel.ProjectACL != nil && len(claGroupModel.ProjectACL) > 0 { - log.WithFields(f).Debugf("adding project_acl: %s", claGroupModel.ProjectACL) - expressionAttributeNames["#A"] = aws.String("project_acl") - expressionAttributeValues[":a"] = &dynamodb.AttributeValue{SS: aws.StringSlice(claGroupModel.ProjectACL)} - updateExpression = updateExpression + " #A = :a, " - } - - if claGroupModel.ProjectICLAEnabled != existingCLAGroup.ProjectICLAEnabled { - log.WithFields(f).Debugf("adding project_icla_enabled: %t", claGroupModel.ProjectICLAEnabled) - expressionAttributeNames["#I"] = aws.String("project_icla_enabled") - expressionAttributeValues[":i"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectICLAEnabled)} - updateExpression = updateExpression + " #I = :i, " - } - - if claGroupModel.ProjectCCLAEnabled != existingCLAGroup.ProjectCCLAEnabled { - log.WithFields(f).Debugf("adding project_ccla_enabled: %t", claGroupModel.ProjectCCLAEnabled) - expressionAttributeNames["#C"] = aws.String("project_ccla_enabled") - expressionAttributeValues[":c"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectCCLAEnabled)} - updateExpression = updateExpression + " #C = :c, " - } - - if claGroupModel.ProjectCCLARequiresICLA != existingCLAGroup.ProjectCCLARequiresICLA { - log.WithFields(f).Debugf("adding project_ccla_requires_icla_signature: %t", claGroupModel.ProjectCCLARequiresICLA) - expressionAttributeNames["#CI"] = aws.String("project_ccla_requires_icla_signature") - expressionAttributeValues[":ci"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectCCLARequiresICLA)} - updateExpression = updateExpression + " #CI = :ci, " - } - - if claGroupModel.ProjectLive != existingCLAGroup.ProjectLive { - log.WithFields(f).Debugf("adding project_live: %t", claGroupModel.ProjectLive) - expressionAttributeNames["#PL"] = aws.String("project_live") - expressionAttributeValues[":pl"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectLive)} - updateExpression = updateExpression + " #PL = :pl, " - } - - _, currentTimeString := utils.CurrentTime() - log.WithFields(f).Debugf("adding date_modified: %s", currentTimeString) - expressionAttributeNames["#M"] = aws.String("date_modified") - expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(currentTimeString)} - updateExpression = updateExpression + " #M = :m " - - // Assemble the query input parameters - updateInput := &dynamodb.UpdateItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "project_id": { - S: aws.String(existingCLAGroup.ProjectID), - }, - }, - ExpressionAttributeNames: expressionAttributeNames, - ExpressionAttributeValues: expressionAttributeValues, - UpdateExpression: &updateExpression, - TableName: aws.String(repo.claGroupTable), - } - //log.Debugf("Update input: %+V", updateInput.GoString()) - - // Make the DynamoDB Update API call - _, updateErr := repo.dynamoDBClient.UpdateItem(updateInput) - if updateErr != nil { - log.WithFields(f).Warnf("error updating CLAGroup by claGroupID: %s, error: %v", claGroupModel.ProjectID, updateErr) - return nil, updateErr - } - - // Read the updated record back from the DB and return - probably could - // just create/update a new model in memory and return it to make it fast, - // but this approach return exactly what the DB has - return repo.GetCLAGroupByID(ctx, claGroupModel.ProjectID, LoadRepoDetails) -} - -func (repo *repo) UpdateRootCLAGroupRepositoriesCount(ctx context.Context, claGroupID string, diff int64, reset bool) error { - f := logrus.Fields{ - "functionName": "UpdateRootCLAGroupRepositoriesCount", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "diff": diff, - "reset": reset, - } - - val := strconv.FormatInt(diff, 10) - expressionAttributeNames := map[string]*string{} - expressionAttributeValues := map[string]*dynamodb.AttributeValue{} - var updateExpression string - - // update root_project_repositories based on reset flag - if reset { - expressionAttributeNames["#R"] = aws.String("root_project_repositories_count") - expressionAttributeValues[":r"] = &dynamodb.AttributeValue{N: aws.String(val)} - updateExpression = "SET #R = :r" - - _, now := utils.CurrentTime() - expressionAttributeNames["#M"] = aws.String("date_modified") - expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(now)} - updateExpression = updateExpression + ", #M = :m" - } else { - expressionAttributeValues[":val"] = &dynamodb.AttributeValue{N: aws.String(val)} - updateExpression = "ADD root_project_repositories_count :val" - } - - input := &dynamodb.UpdateItemInput{ - UpdateExpression: aws.String(updateExpression), - ExpressionAttributeNames: expressionAttributeNames, - ExpressionAttributeValues: expressionAttributeValues, - - Key: map[string]*dynamodb.AttributeValue{ - "project_id": {S: aws.String(claGroupID)}, - }, - - TableName: aws.String(repo.claGroupTable), - } - - _, err := repo.dynamoDBClient.UpdateItem(input) - if err != nil { - log.WithFields(f).WithError(err).Warn("unable to update repositories count") - } - - return err -} - -// buildCLAGroupModels converts the database response model into an API response data model -func (repo *repo) buildCLAGroupModels(ctx context.Context, results []map[string]*dynamodb.AttributeValue, loadRepoDetails bool) ([]models.ClaGroup, error) { - var projects []models.ClaGroup - - // The DB project model - var dbProjects []DBProjectModel - - err := dynamodbattribute.UnmarshalListOfMaps(results, &dbProjects) - if err != nil { - log.Warnf("error unmarshalling projects from database, error: %v", err) - return nil, err - } - - // Create an output channel to receive the results - responseChannel := make(chan *models.ClaGroup) - - // For each project, convert to a response model - using a go routine - for _, dbProject := range dbProjects { - go func(dbProject DBProjectModel) { - // Send the results to the output channel - responseChannel <- repo.buildCLAGroupModel(ctx, dbProject, loadRepoDetails) - }(dbProject) - } - - // Append all the responses to our list - for i := 0; i < len(dbProjects); i++ { - projects = append(projects, *<-responseChannel) - } - - return projects, nil -} - -// buildCLAGroupModel maps the database model to the API response model -func (repo *repo) buildCLAGroupModel(ctx context.Context, dbModel DBProjectModel, loadRepoDetails bool) *models.ClaGroup { - - var ghOrgs []*models.GithubRepositoriesGroupByOrgs - var gerrits []*models.Gerrit - - if loadRepoDetails { - if dbModel.ProjectID != "" { - var wg sync.WaitGroup - wg.Add(2) - go func() { - defer wg.Done() - var err error - ghOrgs, err = repo.ghRepo.GetCLAGroupRepositoriesGroupByOrgs(ctx, dbModel.ProjectID, true) - if err != nil { - log.Warnf("buildPCLAGroupModel - unable to load GH organizations by project ID: %s, error: %+v", - dbModel.ProjectID, err) - // Reset to empty array - ghOrgs = make([]*models.GithubRepositoriesGroupByOrgs, 0) - } - }() - - go func() { - defer wg.Done() - var err error - var gerritsList *models.GerritList - gerritsList, err = repo.gerritRepo.GetClaGroupGerrits(ctx, dbModel.ProjectID, nil) - if err != nil { - log.Warnf("buildCLAGroupModel - unable to load Gerrit repositories by project ID: %s, error: %+v", - dbModel.ProjectID, err) - // Reset to empty array - gerrits = make([]*models.Gerrit, 0) - return - } - gerrits = gerritsList.List - }() - wg.Wait() - } else { - log.Warnf("buildCLAGroupModel - project ID missing for project '%s' - ID: %s - unable to load GH and Gerrit repository details", - dbModel.ProjectName, dbModel.ProjectID) - } - } - - return &models.ClaGroup{ - ProjectID: dbModel.ProjectID, - FoundationSFID: dbModel.FoundationSFID, - RootProjectRepositoriesCount: dbModel.RootProjectRepositoriesCount, - ProjectExternalID: dbModel.ProjectExternalID, - ProjectName: dbModel.ProjectName, - ProjectDescription: dbModel.ProjectDescription, - ProjectACL: dbModel.ProjectACL, - ProjectCCLAEnabled: dbModel.ProjectCclaEnabled, - ProjectICLAEnabled: dbModel.ProjectIclaEnabled, - ProjectCCLARequiresICLA: dbModel.ProjectCclaRequiresIclaSignature, - ProjectLive: dbModel.ProjectLive, - ProjectCorporateDocuments: buildCLAGroupDocumentModels(dbModel.ProjectCorporateDocuments), - ProjectIndividualDocuments: buildCLAGroupDocumentModels(dbModel.ProjectIndividualDocuments), - ProjectMemberDocuments: buildCLAGroupDocumentModels(dbModel.ProjectMemberDocuments), - GithubRepositories: ghOrgs, - Gerrits: gerrits, - DateCreated: dbModel.DateCreated, - DateModified: dbModel.DateModified, - Version: dbModel.Version, - } -} diff --git a/cla-backend-go/project/projections.go b/cla-backend-go/project/repository/projections.go similarity index 94% rename from cla-backend-go/project/projections.go rename to cla-backend-go/project/repository/projections.go index 0fd15fa31..32df11b1b 100644 --- a/cla-backend-go/project/projections.go +++ b/cla-backend-go/project/repository/projections.go @@ -1,7 +1,7 @@ // Copyright The Linux Foundation and each contributor to CommunityBridge. // SPDX-License-Identifier: MIT -package project +package repository import "github.com/aws/aws-sdk-go/service/dynamodb/expression" @@ -24,6 +24,7 @@ func buildProjection() expression.ProjectionBuilder { expression.Name("project_corporate_documents"), expression.Name("project_individual_documents"), expression.Name("project_member_documents"), + expression.Name("project_template_id"), expression.Name("date_created"), expression.Name("date_modified"), expression.Name("version"), diff --git a/cla-backend-go/project/repository/repository.go b/cla-backend-go/project/repository/repository.go new file mode 100644 index 000000000..8d8848f5d --- /dev/null +++ b/cla-backend-go/project/repository/repository.go @@ -0,0 +1,916 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/communitybridge/easycla/cla-backend-go/project/common" + models2 "github.com/communitybridge/easycla/cla-backend-go/project/models" + + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/gerrits" + "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/gen/v1/restapi/operations/project" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/gofrs/uuid" + + "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" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// errors +var ( + ErrProjectDoesNotExist = errors.New("project does not exist") + ErrProjectIDMissing = errors.New("project id is missing") +) + +// constants +const ( + LoadRepoDetails = true + DontLoadRepoDetails = false +) + +// ProjectRepository defines functions of Project repository +type ProjectRepository interface { //nolint + CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) + GetCLAGroupByID(ctx context.Context, claGroupID string, loadRepoDetails bool) (*models.ClaGroup, error) + GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams, loadRepoDetails bool) (*models.ClaGroups, error) + GetCLAGroupByName(ctx context.Context, claGroupName string) (*models.ClaGroup, error) + GetExternalCLAGroup(ctx context.Context, claGroupExternalID string) (*models.ClaGroup, error) + GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) + DeleteCLAGroup(ctx context.Context, claGroupID string) error + UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) + + GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) + GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) + UpdateRootCLAGroupRepositoriesCount(ctx context.Context, claGroupID string, diff int64, reset bool) error +} + +// NewRepository creates instance of project repository +func NewRepository(awsSession *session.Session, stage string, ghRepo repositories.RepositoryInterface, gerritRepo gerrits.Repository, projectClaGroupRepo projects_cla_groups.Repository) ProjectRepository { + return &repo{ + dynamoDBClient: dynamodb.New(awsSession), + stage: stage, + ghRepo: ghRepo, + gerritRepo: gerritRepo, + projectClaGroupRepo: projectClaGroupRepo, + claGroupTable: fmt.Sprintf("cla-%s-projects", stage), + } +} + +type repo struct { + stage string + dynamoDBClient *dynamodb.DynamoDB + ghRepo repositories.RepositoryInterface + gerritRepo gerrits.Repository + projectClaGroupRepo projects_cla_groups.Repository + claGroupTable string +} + +// CreateCLAGroup creates a new CLA Group +func (repo *repo) CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "project.repository.CreateCLAGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectName": claGroupModel.ProjectName, + "projectExternalID": claGroupModel.ProjectExternalID, + "foundationSFID": claGroupModel.FoundationSFID, + "tableName": repo.claGroupTable, + } + // Generate a new CLA Group ID + claGroupID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).Warnf("Unable to generate a UUID for a new CLA Group request, error: %v", err) + return nil, err + } + f["claGroupID"] = claGroupID + + _, currentTimeString := utils.CurrentTime() + input := &dynamodb.PutItemInput{ + Item: map[string]*dynamodb.AttributeValue{}, + TableName: aws.String(repo.claGroupTable), + } + + //var individualDocs []*dynamodb.AttributeValue + //var corporateDocs []*dynamodb.AttributeValue + common.AddStringAttribute(input.Item, "project_id", claGroupID.String()) + common.AddStringAttribute(input.Item, "project_external_id", claGroupModel.ProjectExternalID) + common.AddStringAttribute(input.Item, "foundation_sfid", claGroupModel.FoundationSFID) + common.AddStringAttribute(input.Item, "project_description", claGroupModel.ProjectDescription) + common.AddStringAttribute(input.Item, "project_name", claGroupModel.ProjectName) + common.AddStringAttribute(input.Item, "project_template_id", claGroupModel.ProjectTemplateID) + common.AddStringAttribute(input.Item, "project_name_lower", strings.ToLower(claGroupModel.ProjectName)) + common.AddStringSliceAttribute(input.Item, "project_acl", claGroupModel.ProjectACL) + common.AddBooleanAttribute(input.Item, "project_icla_enabled", claGroupModel.ProjectICLAEnabled) + common.AddBooleanAttribute(input.Item, "project_ccla_enabled", claGroupModel.ProjectCCLAEnabled) + common.AddBooleanAttribute(input.Item, "project_ccla_requires_icla_signature", claGroupModel.ProjectCCLARequiresICLA) + common.AddBooleanAttribute(input.Item, "project_live", claGroupModel.ProjectLive) + + // Empty documents for now - will add the template details later + common.AddListAttribute(input.Item, "project_corporate_documents", []*dynamodb.AttributeValue{}) + common.AddListAttribute(input.Item, "project_individual_documents", []*dynamodb.AttributeValue{}) + common.AddListAttribute(input.Item, "project_member_documents", []*dynamodb.AttributeValue{}) + + common.AddStringAttribute(input.Item, "date_created", currentTimeString) + common.AddStringAttribute(input.Item, "date_modified", currentTimeString) + // Set the version attribute if not already set + if claGroupModel.Version == "" { + claGroupModel.Version = utils.V1 // default value + } + common.AddStringAttribute(input.Item, "version", claGroupModel.Version) + + _, err = repo.dynamoDBClient.PutItem(input) + if err != nil { + log.WithFields(f).Warnf("Unable to create a new CLA Group record, error: %v", err) + return nil, err + } + + // Re-use the provided model - just update the dynamically assigned values + claGroupModel.ProjectID = claGroupID.String() + claGroupModel.DateCreated = currentTimeString + claGroupModel.DateModified = currentTimeString + + return claGroupModel, nil +} + +func (repo *repo) getCLAGroupByID(ctx context.Context, claGroupID string, loadCLAGroupDetails bool) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "project.repository.getCLAGroupByID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "loadProjectDetails": loadCLAGroupDetails, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("loading cla group...") + // This is the key we want to match + condition := expression.Key("project_id").Equal(expression.Value(claGroupID)) + + // Use the builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for CLA Group query, claGroupID: %s, error: %v", + claGroupID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.claGroupTable), + } + + // Make the DynamoDB Query API call + results, queryErr := repo.dynamoDBClient.Query(queryInput) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving cla group by claGroupID: %s, error: %v", claGroupID, queryErr) + return nil, queryErr + } + + if len(results.Items) < 1 { + return nil, &utils.CLAGroupNotFound{CLAGroupID: claGroupID} + } + var dbModel models2.DBProjectModel + err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbModel) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling db cla group model, error: %+v", err) + return nil, err + } + + // Convert the database model to an API response model + return repo.buildCLAGroupModel(ctx, dbModel, loadCLAGroupDetails), nil +} + +// GetCLAGroupByID returns the cla group model associated for the specified claGroupID +func (repo *repo) GetCLAGroupByID(ctx context.Context, claGroupID string, loadRepoDetails bool) (*models.ClaGroup, error) { + return repo.getCLAGroupByID(ctx, claGroupID, loadRepoDetails) +} + +// GetCLAGroupsByExternalID queries the database and returns a list of the cla groups +func (repo *repo) GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams, loadRepoDetails bool) (*models.ClaGroups, error) { + f := logrus.Fields{ + "functionName": "project.repository.GetCLAGroupsByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "ProjectSFID": params.ProjectSFID, + "NextKey": params.NextKey, + "PageSize": params.PageSize, + "loadRepoDetails": loadRepoDetails, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("loading cla group") + + // This is the key we want to match + condition := expression.Key("project_external_id").Equal(expression.Value(params.ProjectSFID)) + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for cla group query, error: %v", err) + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + KeyConditionExpression: expr.KeyCondition(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.claGroupTable), + IndexName: aws.String("external-project-index"), + } + + // If we have the next key, set the exclusive start key value + if params.NextKey != nil && *params.NextKey != "" { + log.WithFields(f).Debugf("Received a nextKey, value: %s", *params.NextKey) + // The primary key of the first item that this operation will evaluate. + // and the query key (if not the same) + queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "project_id": { + S: params.NextKey, + }, + } + } + + var pageSize *int64 + // If we have a page size, set the limit value - make sure it's a positive value + if params.PageSize != nil && *params.PageSize > 0 { + log.WithFields(f).Debugf("Received a pageSize parameter, value: %d", *params.PageSize) + pageSize = params.PageSize + } else { + // Default page size + pageSize = aws.Int64(50) + } + queryInput.Limit = pageSize + + var projects []models.ClaGroup + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving projects, error: %v", errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + projectList, modelErr := repo.buildCLAGroupModels(ctx, results.Items, loadRepoDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting project DB model to response model, error: %v", + modelErr) + return nil, modelErr + } + + // Add to the project response models to the list + projects = append(projects, projectList...) + + if results.LastEvaluatedKey["project_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["project_id"].S + queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "project_id": { + S: aws.String(lastEvaluatedKey), + }, + } + } else { + lastEvaluatedKey = "" + } + + if int64(len(projects)) >= *pageSize { + break + } + } + + return &models.ClaGroups{ + LastKeyScanned: lastEvaluatedKey, + PageSize: *pageSize, + ResultCount: int64(len(projects)), + Projects: projects, + }, nil +} + +// GetClaGroupsByFoundationID queries the database and returns a list of all cla_groups associated with foundation +func (repo *repo) GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) { + f := logrus.Fields{ + "functionName": "project.repository.GetClaGroupsByFoundationSFID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "foundationSFID": foundationSFID, + "loadRepoDetails": loadRepoDetails, + "tableName": repo.claGroupTable, + } + log.WithFields(f).Debugf("loading CLA Group by foundation SFID - using foundation_sfid field...") + + // This is the key we want to match + condition := expression.Key("foundation_sfid").Equal(expression.Value(foundationSFID)) + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project scan, error: %v", err) + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + KeyConditionExpression: expr.KeyCondition(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.claGroupTable), + IndexName: aws.String("foundation-sfid-project-name-index"), + } + + var projects []models.ClaGroup + for { + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving projects, error: %v", errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + projectList, modelErr := repo.buildCLAGroupModels(ctx, results.Items, loadRepoDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting project DB model to response model, error: %v", + modelErr) + return nil, modelErr + } + + // Add to the project response models to the list + projects = append(projects, projectList...) + + if len(results.LastEvaluatedKey) != 0 { + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + break + } + } + + return &models.ClaGroups{ + ResultCount: int64(len(projects)), + Projects: projects, + }, nil +} + +// GetClaGroupByProjectSFID returns cla_group associated with project +func (repo *repo) GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "project.repository.GetClaGroupByProjectSFID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "loadRepoDetails": loadRepoDetails, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("loading project") + + claGroupProject, err := repo.projectClaGroupRepo.GetClaGroupIDForProject(ctx, projectSFID) + if err != nil { + log.WithFields(f).Warnf("error fetching CLA Group ID for project, error: %v", err) + return nil, err + } + + log.WithFields(f).Debugf("found CLA Group ID: %s for project SFID: %s", claGroupProject.ClaGroupID, projectSFID) + + return repo.getCLAGroupByID(ctx, claGroupProject.ClaGroupID, loadRepoDetails) +} + +// GetCLAGroupByName returns the project model associated for the specified project name +func (repo *repo) GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "project.repository.GetCLAGroupByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectName": projectName, + "tableName": repo.claGroupTable, + } + log.WithFields(f).Debugf("loading project") + + // This is the key we want to match + condition := expression.Key("project_name_lower").Equal(expression.Value(strings.ToLower(projectName))) + + // Use the builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).WithError(err).Warnf("error building expression for CLAGroup query, projectName: %s", projectName) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + KeyConditionExpression: expr.KeyCondition(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.claGroupTable), + IndexName: aws.String("project-name-lower-search-index"), + } + + // Make the DynamoDB Query API call + results, queryErr := repo.dynamoDBClient.Query(queryInput) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving project by projectName: %s, error: %v", projectName, queryErr) + return nil, queryErr + } + + // Should only have one result + if *results.Count > 1 { + log.WithFields(f).Warnf("CLAGroup scan by name returned more than one result using projectName: %s", projectName) + } + + // Didn't find it... + if *results.Count == 0 { + log.WithFields(f).Debugf("CLAGroup scan by name returned no results using projectName: %s", projectName) + return nil, nil + } + + // Found it... + var dbModel models2.DBProjectModel + err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbModel) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling db project model, error: %+v", err) + return nil, err + } + + // Convert the database model to an API response model + return repo.buildCLAGroupModel(ctx, dbModel, LoadRepoDetails), nil +} + +// GetExternalCLAGroup returns the project model associated for the specified external project ID +func (repo *repo) GetExternalCLAGroup(ctx context.Context, projectExternalID string) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "project.repository.GetExternalCLAGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectExternalID": projectExternalID, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("loading project") + // This is the key we want to match + condition := expression.Key("project_external_id").Equal(expression.Value(projectExternalID)) + + // Use the builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for CLAGroup query, projectExternalID: %s, error: %v", + projectExternalID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.claGroupTable), + IndexName: aws.String("external-project-index"), + } + + // Make the DynamoDB Query API call + results, queryErr := repo.dynamoDBClient.Query(queryInput) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving project by projectExternalID: %s, error: %v", projectExternalID, queryErr) + return nil, queryErr + } + + // No match, didn't find it + if *results.Count == 0 { + return nil, nil + } + + // Should only have one result + if *results.Count > 1 { + log.WithFields(f).Warnf("CLAGroup query returned more than one result using projectExternalID: %s", projectExternalID) + } + + var dbModel models2.DBProjectModel + err = dynamodbattribute.UnmarshalMap(results.Items[0], &dbModel) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling db project model, error: %+v", err) + return nil, err + } + + // Convert the database model to an API response model + return repo.buildCLAGroupModel(ctx, dbModel, LoadRepoDetails), nil +} + +// GetCLAGroups queries the database and returns a list of the projects +func (repo *repo) GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) { + f := logrus.Fields{ + "functionName": "project.repository.GetCLAGroups", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "searchField": params.SearchField, + "searchTerm": params.SearchTerm, + "nextKey": params.NextKey, + "pageSize": params.PageSize, + "fullMatch": params.FullMatch, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("searching project") + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project scan, error: %v", err) + } + + // Assemble the query input parameters + scanInput := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.claGroupTable), + } + + // If we have the next key, set the exclusive start key value + if params.NextKey != nil && *params.NextKey != "" { + log.WithFields(f).Debugf("Received a nextKey, value: %s", *params.NextKey) + // The primary key of the first item that this operation will evaluate. + // and the query key (if not the same) + scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "project_id": { + S: params.NextKey, + }, + } + } + var pageSize int64 + // If we have a page size, set the limit value - make sure it's a positive value + if params.PageSize != nil && *params.PageSize > 0 { + log.WithFields(f).Debugf("Received a pageSize parameter, value: %d", *params.PageSize) + // The primary key of the first item that this operation will evaluate. + // and the query key (if not the same) + scanInput.Limit = params.PageSize + } else { + // Default page size + pageSize = 50 + params.PageSize = &pageSize + } + + var projects []models.ClaGroup + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + results, errQuery := repo.dynamoDBClient.Scan(scanInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving projects, error: %v", errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + projectList, modelErr := repo.buildCLAGroupModels(ctx, results.Items, DontLoadRepoDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting project DB model to response model, error: %v", + modelErr) + return nil, modelErr + } + + // Add to the project response models to the list + projects = append(projects, projectList...) + + if results.LastEvaluatedKey["project_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["project_id"].S + scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "project_id": { + S: aws.String(lastEvaluatedKey), + }, + } + } else { + lastEvaluatedKey = "" + } + + if int64(len(projects)) >= *params.PageSize { + break + } + } + + return &models.ClaGroups{ + LastKeyScanned: lastEvaluatedKey, + PageSize: *params.PageSize, + Projects: projects, + }, nil +} + +// DeleteCLAGroup deletes the CLAGroup by claGroupID +func (repo *repo) DeleteCLAGroup(ctx context.Context, claGroupID string) error { + f := logrus.Fields{ + "functionName": "project.repository.DeleteCLAGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("deleting CLA Group") + + existingCLAGroup, getErr := repo.GetCLAGroupByID(ctx, claGroupID, DontLoadRepoDetails) + if getErr != nil { + log.WithFields(f).Warnf("delete - error locating the CLA Group, error: %+v", getErr) + return getErr + } + + if existingCLAGroup == nil { + log.WithFields(f).Warn("unable to locate CLA Group by ID - CLA Group does not exist") + return ErrProjectDoesNotExist + } + + var deleteErr error + // Perform the delete + _, deleteErr = repo.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ + TableName: aws.String(repo.claGroupTable), + Key: map[string]*dynamodb.AttributeValue{ + "project_id": { + S: aws.String(existingCLAGroup.ProjectID), + }, + }, + }) + + if deleteErr != nil { + log.WithFields(f).Warnf("Error deleting project with CLA Group ID : %s, error: %v", claGroupID, deleteErr) + return deleteErr + } + + return nil +} + +// UpdateCLAGroup updates the project by claGroupID +func (repo *repo) UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "project.repository.UpdateCLAGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "ProjectID": claGroupModel.ProjectID, + "ProjectName": claGroupModel.ProjectName, + "FoundationSFID": claGroupModel.FoundationSFID, + "ProjectExternalID": claGroupModel.ProjectExternalID, + "ProjectICLAEnabled": claGroupModel.ProjectICLAEnabled, + "ProjectCCLAEnabled": claGroupModel.ProjectCCLAEnabled, + "ProjectCCLARequiresICLA": claGroupModel.ProjectCCLARequiresICLA, + "ProjectTemplateID": claGroupModel.ProjectTemplateID, + "ProjectLive": claGroupModel.ProjectLive, + "tableName": repo.claGroupTable} + log.WithFields(f).Debugf("processing update CLA Group request") + + if claGroupModel.ProjectID == "" { + return nil, ErrProjectIDMissing + } + + existingCLAGroup, getErr := repo.GetCLAGroupByID(ctx, claGroupModel.ProjectID, DontLoadRepoDetails) + if getErr != nil { + log.WithFields(f).Warnf("update - error locating the project id: %s, error: %+v", claGroupModel.ProjectID, getErr) + return nil, getErr + } + + if existingCLAGroup == nil { + return nil, ErrProjectDoesNotExist + } + + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + updateExpression := "SET " + + // An update to the CLA Group name... + if claGroupModel.ProjectName != "" && existingCLAGroup.ProjectName != claGroupModel.ProjectName { + log.WithFields(f).Debugf("adding project_name: %s", claGroupModel.ProjectName) + expressionAttributeNames["#N"] = aws.String("project_name") + expressionAttributeValues[":n"] = &dynamodb.AttributeValue{S: aws.String(claGroupModel.ProjectName)} + updateExpression = updateExpression + " #N = :n, " + log.WithFields(f).Debugf("adding project name lower: %s", strings.ToLower(claGroupModel.ProjectName)) + expressionAttributeNames["#LOW"] = aws.String("project_name_lower") + expressionAttributeValues[":low"] = &dynamodb.AttributeValue{S: aws.String(strings.ToLower(claGroupModel.ProjectName))} + updateExpression = updateExpression + " #LOW = :low, " + } + + // An update to the CLA Group description... + if existingCLAGroup.ProjectDescription != claGroupModel.ProjectDescription { + log.WithFields(f).Debugf("adding project_description: %s", claGroupModel.ProjectDescription) + expressionAttributeNames["#DESC"] = aws.String("project_description") + expressionAttributeValues[":desc"] = &dynamodb.AttributeValue{S: aws.String(claGroupModel.ProjectDescription)} + updateExpression = updateExpression + " #DESC = :desc, " + } + + // An update to the project ACL + if claGroupModel.ProjectACL != nil && len(claGroupModel.ProjectACL) > 0 { + log.WithFields(f).Debugf("adding project_acl: %s", claGroupModel.ProjectACL) + expressionAttributeNames["#A"] = aws.String("project_acl") + expressionAttributeValues[":a"] = &dynamodb.AttributeValue{SS: aws.StringSlice(claGroupModel.ProjectACL)} + updateExpression = updateExpression + " #A = :a, " + } + + // An update to the ICLA enabled flag + if claGroupModel.ProjectICLAEnabled != existingCLAGroup.ProjectICLAEnabled { + log.WithFields(f).Debugf("adding project_icla_enabled: %t", claGroupModel.ProjectICLAEnabled) + expressionAttributeNames["#I"] = aws.String("project_icla_enabled") + expressionAttributeValues[":i"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectICLAEnabled)} + updateExpression = updateExpression + " #I = :i, " + } + + // An update to the CCLA enabled flag + if claGroupModel.ProjectCCLAEnabled != existingCLAGroup.ProjectCCLAEnabled { + log.WithFields(f).Debugf("adding project_ccla_enabled: %t", claGroupModel.ProjectCCLAEnabled) + expressionAttributeNames["#C"] = aws.String("project_ccla_enabled") + expressionAttributeValues[":c"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectCCLAEnabled)} + updateExpression = updateExpression + " #C = :c, " + } + + // An update to the CCLA requires ICLA flag + if claGroupModel.ProjectCCLARequiresICLA != existingCLAGroup.ProjectCCLARequiresICLA { + log.WithFields(f).Debugf("adding project_ccla_requires_icla_signature: %t", claGroupModel.ProjectCCLARequiresICLA) + expressionAttributeNames["#CI"] = aws.String("project_ccla_requires_icla_signature") + expressionAttributeValues[":ci"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectCCLARequiresICLA)} + updateExpression = updateExpression + " #CI = :ci, " + } + + // An update to the project live flag + if claGroupModel.ProjectLive != existingCLAGroup.ProjectLive { + log.WithFields(f).Debugf("adding project_live: %t", claGroupModel.ProjectLive) + expressionAttributeNames["#PL"] = aws.String("project_live") + expressionAttributeValues[":pl"] = &dynamodb.AttributeValue{BOOL: aws.Bool(claGroupModel.ProjectLive)} + updateExpression = updateExpression + " #PL = :pl, " + } + + // We'll update the date modified time + _, currentTimeString := utils.CurrentTime() + log.WithFields(f).Debugf("adding date_modified: %s", currentTimeString) + expressionAttributeNames["#M"] = aws.String("date_modified") + expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(currentTimeString)} + updateExpression = updateExpression + " #M = :m " + + // Assemble the query input parameters + updateInput := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "project_id": { + S: aws.String(existingCLAGroup.ProjectID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(repo.claGroupTable), + } + + // Make the DynamoDB Update API call + _, updateErr := repo.dynamoDBClient.UpdateItem(updateInput) + if updateErr != nil { + log.WithFields(f).Warnf("error updating CLAGroup by claGroupID: %s, error: %v", claGroupModel.ProjectID, updateErr) + return nil, updateErr + } + + // Read the updated record back from the DB and return - probably could + // just create/update a new model in memory and return it to make it fast, + // but this approach return exactly what the DB has + return repo.GetCLAGroupByID(ctx, claGroupModel.ProjectID, LoadRepoDetails) +} + +func (repo *repo) UpdateRootCLAGroupRepositoriesCount(ctx context.Context, claGroupID string, diff int64, reset bool) error { + f := logrus.Fields{ + "functionName": "project.repository.UpdateRootCLAGroupRepositoriesCount", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "diff": diff, + "reset": reset, + } + + val := strconv.FormatInt(diff, 10) + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + var updateExpression string + + // update root_project_repositories based on reset flag + if reset { + expressionAttributeNames["#R"] = aws.String("root_project_repositories_count") + expressionAttributeValues[":r"] = &dynamodb.AttributeValue{N: aws.String(val)} + updateExpression = "SET #R = :r" + + _, now := utils.CurrentTime() + expressionAttributeNames["#M"] = aws.String("date_modified") + expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(now)} + updateExpression = updateExpression + ", #M = :m" + } else { + expressionAttributeValues[":val"] = &dynamodb.AttributeValue{N: aws.String(val)} + updateExpression = "ADD root_project_repositories_count :val" + } + + input := &dynamodb.UpdateItemInput{ + UpdateExpression: aws.String(updateExpression), + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + + Key: map[string]*dynamodb.AttributeValue{ + "project_id": {S: aws.String(claGroupID)}, + }, + + TableName: aws.String(repo.claGroupTable), + } + + _, err := repo.dynamoDBClient.UpdateItem(input) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to update repositories count") + } + + return err +} + +// buildCLAGroupModels converts the database response model into an API response data model +func (repo *repo) buildCLAGroupModels(ctx context.Context, results []map[string]*dynamodb.AttributeValue, loadRepoDetails bool) ([]models.ClaGroup, error) { + var projects []models.ClaGroup + + // The DB project model + var dbProjects []models2.DBProjectModel + + err := dynamodbattribute.UnmarshalListOfMaps(results, &dbProjects) + if err != nil { + log.Warnf("error unmarshalling projects from database, error: %v", err) + return nil, err + } + + // Create an output channel to receive the results + responseChannel := make(chan *models.ClaGroup) + + // For each project, convert to a response model - using a go routine + for _, dbProject := range dbProjects { + go func(dbProject models2.DBProjectModel) { + // Send the results to the output channel + responseChannel <- repo.buildCLAGroupModel(ctx, dbProject, loadRepoDetails) + }(dbProject) + } + + // Append all the responses to our list + for i := 0; i < len(dbProjects); i++ { + projects = append(projects, *<-responseChannel) + } + + return projects, nil +} + +// buildCLAGroupModel maps the database model to the API response model +func (repo *repo) buildCLAGroupModel(ctx context.Context, dbModel models2.DBProjectModel, loadRepoDetails bool) *models.ClaGroup { + + var ghOrgs []*models.GithubRepositoriesGroupByOrgs + var gerrits []*models.Gerrit + + if loadRepoDetails { + if dbModel.ProjectID != "" { + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + var err error + ghOrgs, err = repo.ghRepo.GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx, dbModel.ProjectID, true) + if err != nil { + log.Warnf("buildPCLAGroupModel - unable to load GH organizations by project ID: %s, error: %+v", + dbModel.ProjectID, err) + // Reset to empty array + ghOrgs = make([]*models.GithubRepositoriesGroupByOrgs, 0) + } + }() + + go func() { + defer wg.Done() + var err error + var gerritsList *models.GerritList + gerritsList, err = repo.gerritRepo.GetClaGroupGerrits(ctx, dbModel.ProjectID) + if err != nil { + log.Warnf("buildCLAGroupModel - unable to load Gerrit repositories by project ID: %s, error: %+v", + dbModel.ProjectID, err) + // Reset to empty array + gerrits = make([]*models.Gerrit, 0) + return + } + gerrits = gerritsList.List + }() + wg.Wait() + } else { + log.Warnf("buildCLAGroupModel - project ID missing for project '%s' - ID: %s - unable to load GH and Gerrit repository details", + dbModel.ProjectName, dbModel.ProjectID) + } + } + + return &models.ClaGroup{ + ProjectID: dbModel.ProjectID, + FoundationSFID: dbModel.FoundationSFID, + RootProjectRepositoriesCount: dbModel.RootProjectRepositoriesCount, + ProjectExternalID: dbModel.ProjectExternalID, + ProjectName: dbModel.ProjectName, + ProjectDescription: dbModel.ProjectDescription, + ProjectACL: dbModel.ProjectACL, + ProjectCCLAEnabled: dbModel.ProjectCclaEnabled, + ProjectICLAEnabled: dbModel.ProjectIclaEnabled, + ProjectCCLARequiresICLA: dbModel.ProjectCclaRequiresIclaSignature, + ProjectTemplateID: dbModel.ProjectTemplateID, + ProjectLive: dbModel.ProjectLive, + ProjectCorporateDocuments: common.BuildCLAGroupDocumentModels(dbModel.ProjectCorporateDocuments), + ProjectIndividualDocuments: common.BuildCLAGroupDocumentModels(dbModel.ProjectIndividualDocuments), + ProjectMemberDocuments: common.BuildCLAGroupDocumentModels(dbModel.ProjectMemberDocuments), + GithubRepositories: ghOrgs, + Gerrits: gerrits, + DateCreated: dbModel.DateCreated, + DateModified: dbModel.DateModified, + Version: dbModel.Version, + } +} diff --git a/cla-backend-go/project/service.go b/cla-backend-go/project/service.go deleted file mode 100644 index 3819eae79..000000000 --- a/cla-backend-go/project/service.go +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package project - -import ( - "context" - "sync" - - "github.com/communitybridge/easycla/cla-backend-go/users" - - "github.com/communitybridge/easycla/cla-backend-go/utils" - - "github.com/sirupsen/logrus" - - "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/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/project" - "github.com/communitybridge/easycla/cla-backend-go/gerrits" - log "github.com/communitybridge/easycla/cla-backend-go/logging" -) - -// Service interface defines the project service methods/functions -type Service interface { - CreateCLAGroup(ctx context.Context, project *models.ClaGroup) (*models.ClaGroup, error) - GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) - GetCLAGroupByID(ctx context.Context, claGroupID string) (*models.ClaGroup, error) - GetCLAGroupsByExternalSFID(ctx context.Context, projectSFID string) (*models.ClaGroups, error) - GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams) (*models.ClaGroups, error) - GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) - GetCLAGroupCurrentICLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) - GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) - DeleteCLAGroup(ctx context.Context, claGroupID string) error - UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) - GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) - GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) - SignedAtFoundationLevel(ctx context.Context, foundationSFID string) (bool, error) - GetCLAManagers(ctx context.Context, claGroupID string) ([]*models.ClaManagerUser, error) -} - -// service -type service struct { - repo ProjectRepository - repositoriesRepo repositories.Repository - gerritRepo gerrits.Repository - projectCGRepo projects_cla_groups.Repository - usersRepo users.UserRepository -} - -// NewService returns an instance of the project service -func NewService(projectRepo ProjectRepository, repositoriesRepo repositories.Repository, gerritRepo gerrits.Repository, pcgRepo projects_cla_groups.Repository, usersRepo users.UserRepository) Service { - return service{ - repo: projectRepo, - repositoriesRepo: repositoriesRepo, - gerritRepo: gerritRepo, - projectCGRepo: pcgRepo, - usersRepo: usersRepo, - } -} - -// CreateProject service method -func (s service) CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { - return s.repo.CreateCLAGroup(ctx, claGroupModel) -} - -// GetCLAGroups service method -func (s service) GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) { - return s.repo.GetCLAGroups(ctx, params) -} - -// GetProjectByID service method -func (s service) GetCLAGroupByID(ctx context.Context, claGroupID string) (*models.ClaGroup, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroupByID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "loadRepoDetails": LoadRepoDetails, - } - - log.WithFields(f).Debug("locating CLA Group by ID...") - project, err := s.repo.GetCLAGroupByID(ctx, claGroupID, LoadRepoDetails) - if err != nil { - return nil, err - } - - // No Foundation SFID value? Maybe this is a v1 CLA Group record... - if project.FoundationSFID == "" { - log.WithFields(f).Debug("CLA Group missing FoundationSFID...") - // Most likely this is a CLA Group v1 record - use the external ID if available - if project.ProjectExternalID != "" { - log.WithFields(f).Debugf("CLA Group assigning foundationID to value of external ID: %s", project.ProjectExternalID) - project.FoundationSFID = project.ProjectExternalID - } - } - - if project.FoundationSFID != "" { - signed, checkErr := s.SignedAtFoundationLevel(ctx, project.FoundationSFID) - if checkErr != nil { - return nil, checkErr - } - project.FoundationLevelCLA = signed - } - - return project, nil -} - -// GetCLAGroupsByExternalSFID returns a list of projects based on the external SFID parameter -func (s service) GetCLAGroupsByExternalSFID(ctx context.Context, projectSFID string) (*models.ClaGroups, error) { - return s.GetCLAGroupsByExternalID(ctx, &project.GetProjectsByExternalIDParams{ - HTTPRequest: nil, - NextKey: nil, - PageSize: nil, - ProjectSFID: projectSFID, - }) -} - -// GetCLAGroupsByExternalID returns a list of projects based on the external ID parameters -func (s service) GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams) (*models.ClaGroups, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroupsByExternalID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectSFID": params.ProjectSFID, - "NextKey": params.NextKey, - "PageSize": params.PageSize} - log.Debugf("Project Service Handler - GetCLAGroupsByExternalID") - projects, err := s.repo.GetCLAGroupsByExternalID(ctx, params, LoadRepoDetails) - if err != nil { - log.WithFields(f).Warnf("problem with query, error: %+v", err) - return nil, err - } - numberOfProjects := len(projects.Projects) - if numberOfProjects == 0 { - return projects, nil - } - - // Add repository information in the response model - var wg sync.WaitGroup - wg.Add(numberOfProjects) - for i := range projects.Projects { - go func(project *models.ClaGroup) { - defer wg.Done() - s.fillRepoInfo(ctx, project) - }(&projects.Projects[i]) - } - wg.Wait() - - return projects, nil -} - -// GetCLAGroupByName service method -func (s service) GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) { - return s.repo.GetCLAGroupByName(ctx, projectName) -} - -func (s service) GetCLAGroupCurrentICLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroupCurrentICLATemplateURLByID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - } - - claGroupModel, err := s.GetCLAGroupByID(ctx, claGroupID) - if err != nil { - log.WithFields(f).WithError(err).Warn("unable to load CLA Group by ID") - return "", &utils.CLAGroupNotFound{ - CLAGroupID: claGroupID, - Err: err, - } - } - - if claGroupModel == nil { - log.WithFields(f).Warn("unable to load CLA Group by ID") - return "", &utils.CLAGroupNotFound{ - CLAGroupID: claGroupID, - Err: nil, - } - } - f["claGroupName"] = claGroupModel.ProjectName - - if !claGroupModel.ProjectICLAEnabled { - log.WithFields(f).Warn("ICLA is not configured for this CLA Group - unable to return ICLA template URL") - return "", &utils.CLAGroupICLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: nil, - } - } - - docs := claGroupModel.ProjectIndividualDocuments - if len(docs) == 0 { - log.WithFields(f).Warn("ICLA is not configured for this CLA Group - missing document configuration") - return "", &utils.CLAGroupICLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: nil, - } - } - - // Fetch the current document - currentDoc, err := GetCurrentDocument(ctx, docs) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group") - return "", &utils.CLAGroupICLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: err, - } - } - - if currentDoc == (models.ClaGroupDocument{}) { - log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group") - return "", &utils.CLAGroupICLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: err, - } - } - - if currentDoc.DocumentS3URL == "" { - log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group - document s3 url is empty") - return "", &utils.CLAGroupICLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: err, - } - } - - return currentDoc.DocumentS3URL, nil -} - -func (s service) GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) { - f := logrus.Fields{ - "functionName": "GetCLAGroupCurrentCCLATemplateURLByID", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - } - - claGroupModel, err := s.GetCLAGroupByID(ctx, claGroupID) - if err != nil { - log.WithFields(f).WithError(err).Warn("unable to load CLA Group by ID") - return "", &utils.CLAGroupNotFound{ - CLAGroupID: claGroupID, - Err: err, - } - } - - if claGroupModel == nil { - log.WithFields(f).Warn("unable to load CLA Group by ID") - return "", &utils.CLAGroupNotFound{ - CLAGroupID: claGroupID, - Err: nil, - } - } - f["claGroupName"] = claGroupModel.ProjectName - - if !claGroupModel.ProjectCCLAEnabled { - log.WithFields(f).Warn("CCLA is not configured for this CLA Group - unable to return CCLA template URL") - return "", &utils.CLAGroupCCLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: nil, - } - } - - docs := claGroupModel.ProjectCorporateDocuments - if len(docs) == 0 { - log.WithFields(f).Warn("CCLA is not configured for this CLA Group - missing document configuration") - return "", &utils.CLAGroupCCLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: nil, - } - } - - // Fetch the current document - currentDoc, err := GetCurrentDocument(ctx, docs) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group") - return "", &utils.CLAGroupCCLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: err, - } - } - - if currentDoc == (models.ClaGroupDocument{}) { - log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group") - return "", &utils.CLAGroupCCLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: err, - } - } - - if currentDoc.DocumentS3URL == "" { - log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group - document s3 url is empty") - return "", &utils.CLAGroupCCLANotConfigured{ - CLAGroupID: claGroupID, - CLAGroupName: claGroupModel.ProjectName, - Err: err, - } - } - - return currentDoc.DocumentS3URL, nil -} - -// DeleteCLAGroup service method -func (s service) DeleteCLAGroup(ctx context.Context, claGroupID string) error { - return s.repo.DeleteCLAGroup(ctx, claGroupID) -} - -// UpdateCLAGroup service method -func (s service) UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { - // Updates to the CLA Group "projects" table will cause a DB trigger handler (separate lambda) to also update other - // tables where we have the CLA Group name/description - return s.repo.UpdateCLAGroup(ctx, claGroupModel) -} - -// GetClaGroupsByFoundationSFID service method -func (s service) GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) { - return s.repo.GetClaGroupsByFoundationSFID(ctx, foundationSFID, loadRepoDetails) -} - -// GetClaGroupByProjectSFID( service method -func (s service) GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) { - return s.repo.GetClaGroupByProjectSFID(ctx, projectSFID, loadRepoDetails) -} - -// SignedAtFoundationLevel returns true if the specified foundation has a CLA Group at the foundation level, returns false otherwise. -func (s service) SignedAtFoundationLevel(ctx context.Context, foundationSFID string) (bool, error) { - f := logrus.Fields{ - "functionName": "SignedAtFoundationLevel", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "foundationSFID": foundationSFID, - } - - log.WithFields(f).Debug("querying foundation CLA Group entries...") - entries, pcgErr := s.projectCGRepo.GetProjectsIdsForFoundation(foundationSFID) - if pcgErr != nil { - return false, pcgErr - } - log.WithFields(f).Debugf("loaded %d CLA Group entries", len(entries)) - - // Check for number of claGroups for foundation - foundationLevelCLAGroup := false - for _, entry := range entries { - if entry.ProjectSFID == entry.FoundationSFID { - foundationLevelCLAGroup = true - break - } - } - - return foundationLevelCLAGroup, nil -} - -// GetCLAManagers retrieves a list of managers for the give claGroupID -func (s service) GetCLAManagers(ctx context.Context, claGroupID string) ([]*models.ClaManagerUser, error) { - claGroupModel, err := s.GetCLAGroupByID(ctx, claGroupID) - if err != nil { - return nil, err - } - - if len(claGroupModel.ProjectACL) == 0 { - return nil, nil - } - - var managers []*models.ClaManagerUser - for _, lfUserName := range claGroupModel.ProjectACL { - log.Debugf("getting cla manager user : %s", lfUserName) - u, err := s.usersRepo.GetUserByLFUserName(lfUserName) - if err != nil { - log.Warnf("fetching the user with lfUserName : %s failed : %v", lfUserName, err) - return nil, err - } - managers = append(managers, &models.ClaManagerUser{ - UserEmail: u.LfEmail, - UserLFID: u.LfUsername, - UserName: u.Username, - }) - } - - return managers, nil -} diff --git a/cla-backend-go/project/service/service.go b/cla-backend-go/project/service/service.go new file mode 100644 index 000000000..f6a303e35 --- /dev/null +++ b/cla-backend-go/project/service/service.go @@ -0,0 +1,428 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package service + +import ( + "context" + "sync" + + "github.com/communitybridge/easycla/cla-backend-go/project/common" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + + "github.com/communitybridge/easycla/cla-backend-go/users" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + + "github.com/sirupsen/logrus" + + "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/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/project" + "github.com/communitybridge/easycla/cla-backend-go/gerrits" + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +// Service interface defines the project service methods/functions +type Service interface { + CreateCLAGroup(ctx context.Context, project *models.ClaGroup) (*models.ClaGroup, error) + GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) + GetCLAGroupByID(ctx context.Context, claGroupID string) (*models.ClaGroup, error) + GetCLAGroupsByExternalSFID(ctx context.Context, projectSFID string) (*models.ClaGroups, error) + GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams) (*models.ClaGroups, error) + GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) + GetCLAGroupCurrentICLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) + GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) + DeleteCLAGroup(ctx context.Context, claGroupID string) error + UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) + GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) + GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) + SignedAtFoundationLevel(ctx context.Context, foundationSFID string) (bool, error) + GetCLAManagers(ctx context.Context, claGroupID string) ([]*models.ClaManagerUser, error) +} + +// ProjectService project service data model +type ProjectService struct { + repo repository.ProjectRepository + repositoriesRepo repositories.RepositoryInterface + gerritRepo gerrits.Repository + projectCLAGroupRepo projects_cla_groups.Repository + usersRepo users.UserRepository +} + +// NewService returns an instance of the project service +func NewService(projectRepo repository.ProjectRepository, repositoriesRepo repositories.RepositoryInterface, gerritRepo gerrits.Repository, projectCLAGroupRepo projects_cla_groups.Repository, usersRepo users.UserRepository) Service { + return ProjectService{ + repo: projectRepo, + repositoriesRepo: repositoriesRepo, + gerritRepo: gerritRepo, + projectCLAGroupRepo: projectCLAGroupRepo, + usersRepo: usersRepo, + } +} + +// CreateCLAGroup service method +func (s ProjectService) CreateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + return s.repo.CreateCLAGroup(ctx, claGroupModel) +} + +// GetCLAGroups service method +func (s ProjectService) GetCLAGroups(ctx context.Context, params *project.GetProjectsParams) (*models.ClaGroups, error) { + return s.repo.GetCLAGroups(ctx, params) +} + +// GetCLAGroupByID service method +func (s ProjectService) GetCLAGroupByID(ctx context.Context, claGroupID string) (*models.ClaGroup, error) { + f := logrus.Fields{ + "functionName": "GetCLAGroupByID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "loadRepoDetails": repository.LoadRepoDetails, + } + + log.WithFields(f).Debug("locating CLA Group by ID...") + project, err := s.repo.GetCLAGroupByID(ctx, claGroupID, repository.LoadRepoDetails) + if err != nil { + return nil, err + } + + // No Foundation SFID value? Maybe this is a v1 CLA Group record... + if project.FoundationSFID == "" { + log.WithFields(f).Debug("CLA Group missing FoundationSFID...") + // Most likely this is a CLA Group v1 record - use the external ID if available + if project.ProjectExternalID != "" { + log.WithFields(f).Debugf("CLA Group assigning foundationID to value of external ID: %s", project.ProjectExternalID) + project.FoundationSFID = project.ProjectExternalID + } + } + + if project.FoundationSFID != "" { + signed, checkErr := s.SignedAtFoundationLevel(ctx, project.FoundationSFID) + if checkErr != nil { + return nil, checkErr + } + project.FoundationLevelCLA = signed + } + + return project, nil +} + +// GetCLAGroupsByExternalSFID returns a list of projects based on the external SFID parameter +func (s ProjectService) GetCLAGroupsByExternalSFID(ctx context.Context, projectSFID string) (*models.ClaGroups, error) { + return s.GetCLAGroupsByExternalID(ctx, &project.GetProjectsByExternalIDParams{ + HTTPRequest: nil, + NextKey: nil, + PageSize: nil, + ProjectSFID: projectSFID, + }) +} + +// GetCLAGroupsByExternalID returns a list of projects based on the external ID parameters +func (s ProjectService) GetCLAGroupsByExternalID(ctx context.Context, params *project.GetProjectsByExternalIDParams) (*models.ClaGroups, error) { + f := logrus.Fields{ + "functionName": "GetCLAGroupsByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "NextKey": params.NextKey, + "PageSize": params.PageSize} + log.Debugf("Project Service Handler - GetCLAGroupsByExternalID") + projects, err := s.repo.GetCLAGroupsByExternalID(ctx, params, repository.LoadRepoDetails) + if err != nil { + log.WithFields(f).Warnf("problem with query, error: %+v", err) + return nil, err + } + numberOfProjects := len(projects.Projects) + if numberOfProjects == 0 { + return projects, nil + } + + // Add repository information in the response model + var wg sync.WaitGroup + wg.Add(numberOfProjects) + for i := range projects.Projects { + go func(project *models.ClaGroup) { + defer wg.Done() + s.FillRepoInfo(ctx, project) + }(&projects.Projects[i]) + } + wg.Wait() + + return projects, nil +} + +// GetCLAGroupByName service method +func (s ProjectService) GetCLAGroupByName(ctx context.Context, projectName string) (*models.ClaGroup, error) { + return s.repo.GetCLAGroupByName(ctx, projectName) +} + +func (s ProjectService) GetCLAGroupCurrentICLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) { + f := logrus.Fields{ + "functionName": "GetCLAGroupCurrentICLATemplateURLByID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + } + + claGroupModel, err := s.GetCLAGroupByID(ctx, claGroupID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load CLA Group by ID") + return "", &utils.CLAGroupNotFound{ + CLAGroupID: claGroupID, + Err: err, + } + } + + if claGroupModel == nil { + log.WithFields(f).Warn("unable to load CLA Group by ID") + return "", &utils.CLAGroupNotFound{ + CLAGroupID: claGroupID, + Err: nil, + } + } + f["claGroupName"] = claGroupModel.ProjectName + + if !claGroupModel.ProjectICLAEnabled { + log.WithFields(f).Warn("ICLA is not configured for this CLA Group - unable to return ICLA template URL") + return "", &utils.CLAGroupICLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: nil, + } + } + + docs := claGroupModel.ProjectIndividualDocuments + if len(docs) == 0 { + log.WithFields(f).Warn("ICLA is not configured for this CLA Group - missing document configuration") + return "", &utils.CLAGroupICLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: nil, + } + } + + // Fetch the current document + currentDoc, err := common.GetCurrentDocument(ctx, docs) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group") + return "", &utils.CLAGroupICLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: err, + } + } + + if common.AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group - document is empty") + return "", &utils.CLAGroupICLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: err, + } + } + + if currentDoc.DocumentS3URL == "" { + log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group - document s3 url is empty") + return "", &utils.CLAGroupICLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: err, + } + } + + return currentDoc.DocumentS3URL, nil +} + +func (s ProjectService) GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Context, claGroupID string) (string, error) { + f := logrus.Fields{ + "functionName": "GetCLAGroupCurrentCCLATemplateURLByID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + } + + claGroupModel, err := s.GetCLAGroupByID(ctx, claGroupID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to load CLA Group by ID") + return "", &utils.CLAGroupNotFound{ + CLAGroupID: claGroupID, + Err: err, + } + } + + if claGroupModel == nil { + log.WithFields(f).Warn("unable to load CLA Group by ID") + return "", &utils.CLAGroupNotFound{ + CLAGroupID: claGroupID, + Err: nil, + } + } + f["claGroupName"] = claGroupModel.ProjectName + + if !claGroupModel.ProjectCCLAEnabled { + log.WithFields(f).Warn("CCLA is not configured for this CLA Group - unable to return CCLA template URL") + return "", &utils.CLAGroupCCLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: nil, + } + } + + docs := claGroupModel.ProjectCorporateDocuments + if len(docs) == 0 { + log.WithFields(f).Warn("CCLA is not configured for this CLA Group - missing document configuration") + return "", &utils.CLAGroupCCLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: nil, + } + } + + // Fetch the current document + currentDoc, err := common.GetCurrentDocument(ctx, docs) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group") + return "", &utils.CLAGroupCCLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: err, + } + } + + if common.AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group - document is empty") + return "", &utils.CLAGroupCCLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: err, + } + } + + if currentDoc.DocumentS3URL == "" { + log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group - document s3 url is empty") + return "", &utils.CLAGroupCCLANotConfigured{ + CLAGroupID: claGroupID, + CLAGroupName: claGroupModel.ProjectName, + Err: err, + } + } + + return currentDoc.DocumentS3URL, nil +} + +// DeleteCLAGroup service method +func (s ProjectService) DeleteCLAGroup(ctx context.Context, claGroupID string) error { + return s.repo.DeleteCLAGroup(ctx, claGroupID) +} + +// UpdateCLAGroup service method +func (s ProjectService) UpdateCLAGroup(ctx context.Context, claGroupModel *models.ClaGroup) (*models.ClaGroup, error) { + // Updates to the CLA Group "projects" table will cause a DB trigger handler (separate lambda) to also update other + // tables where we have the CLA Group name/description + return s.repo.UpdateCLAGroup(ctx, claGroupModel) +} + +// GetClaGroupsByFoundationSFID service method +func (s ProjectService) GetClaGroupsByFoundationSFID(ctx context.Context, foundationSFID string, loadRepoDetails bool) (*models.ClaGroups, error) { + return s.repo.GetClaGroupsByFoundationSFID(ctx, foundationSFID, loadRepoDetails) +} + +// GetClaGroupByProjectSFID service method +func (s ProjectService) GetClaGroupByProjectSFID(ctx context.Context, projectSFID string, loadRepoDetails bool) (*models.ClaGroup, error) { + return s.repo.GetClaGroupByProjectSFID(ctx, projectSFID, loadRepoDetails) +} + +// SignedAtFoundationLevel returns true if the specified foundation has a CLA Group at the foundation level, returns false otherwise. +func (s ProjectService) SignedAtFoundationLevel(ctx context.Context, foundationSFID string) (bool, error) { + f := logrus.Fields{ + "functionName": "SignedAtFoundationLevel", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "foundationSFID": foundationSFID, + } + + log.WithFields(f).Debug("querying foundation CLA Group entries...") + entries, pcgErr := s.projectCLAGroupRepo.GetProjectsIdsForFoundation(ctx, foundationSFID) + if pcgErr != nil { + return false, pcgErr + } + log.WithFields(f).Debugf("loaded %d CLA Group entries signed at foundation level...", len(entries)) + + // Check for number of claGroups for foundation + foundationLevelCLAGroup := false + for _, entry := range entries { + if entry.ProjectSFID == entry.FoundationSFID { + foundationLevelCLAGroup = true + break + } + } + + log.WithFields(f).Debugf("returning %t for signed at foundation level for: %s", foundationLevelCLAGroup, foundationSFID) + return foundationLevelCLAGroup, nil +} + +// GetCLAManagers retrieves a list of managers for the give claGroupID +func (s ProjectService) GetCLAManagers(ctx context.Context, claGroupID string) ([]*models.ClaManagerUser, error) { + claGroupModel, err := s.GetCLAGroupByID(ctx, claGroupID) + if err != nil { + return nil, err + } + + if len(claGroupModel.ProjectACL) == 0 { + return nil, nil + } + + var managers []*models.ClaManagerUser + for _, lfUserName := range claGroupModel.ProjectACL { + log.Debugf("getting cla manager user : %s", lfUserName) + u, err := s.usersRepo.GetUserByLFUserName(lfUserName) + if err != nil { + log.Warnf("fetching the user with lfUserName : %s failed : %v", lfUserName, err) + return nil, err + } + managers = append(managers, &models.ClaManagerUser{ + UserEmail: u.LfEmail.String(), + UserLFID: u.LfUsername, + UserName: u.Username, + }) + } + + return managers, nil +} + +// FillRepoInfo helper function to fill the repository info +func (s ProjectService) FillRepoInfo(ctx context.Context, project *models.ClaGroup) { + f := logrus.Fields{ + "functionName": "v1.project.helpers.fillRepoInfo", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + var wg sync.WaitGroup + wg.Add(2) + var ghrepos []*models.GithubRepositoriesGroupByOrgs + var gerrits []*models.Gerrit + + go func() { + defer wg.Done() + var err error + ghrepos, err = s.repositoriesRepo.GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx, project.ProjectID, true) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get github repositories for cla group ID: %s", project.ProjectID) + return + } + }() + + go func() { + defer wg.Done() + var err error + var gerritsList *models.GerritList + gerritsList, err = s.gerritRepo.GetClaGroupGerrits(ctx, project.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get gerrit instances for cla group ID: %s.", project.ProjectID) + return + } + gerrits = gerritsList.List + }() + + wg.Wait() + project.GithubRepositories = ghrepos + project.Gerrits = gerrits +} diff --git a/cla-backend-go/projects_cla_groups/mocks/mock_repository.go b/cla-backend-go/projects_cla_groups/mocks/mock_repository.go new file mode 100644 index 000000000..9035fdf63 --- /dev/null +++ b/cla-backend-go/projects_cla_groups/mocks/mock_repository.go @@ -0,0 +1,215 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: projects_cla_groups/repository.go + +// Package mock_projects_cla_groups is a generated GoMock package. +package mock_projects_cla_groups + +import ( + context "context" + reflect "reflect" + + projects_cla_groups "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// AssociateClaGroupWithProject mocks base method. +func (m *MockRepository) AssociateClaGroupWithProject(ctx context.Context, claGroupID, projectSFID, foundationSFID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssociateClaGroupWithProject", ctx, claGroupID, projectSFID, foundationSFID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AssociateClaGroupWithProject indicates an expected call of AssociateClaGroupWithProject. +func (mr *MockRepositoryMockRecorder) AssociateClaGroupWithProject(ctx, claGroupID, projectSFID, foundationSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssociateClaGroupWithProject", reflect.TypeOf((*MockRepository)(nil).AssociateClaGroupWithProject), ctx, claGroupID, projectSFID, foundationSFID) +} + +// GetCLAGroup mocks base method. +func (m *MockRepository) GetCLAGroup(ctx context.Context, claGroupID string) (*projects_cla_groups.ProjectClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroup", ctx, claGroupID) + ret0, _ := ret[0].(*projects_cla_groups.ProjectClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroup indicates an expected call of GetCLAGroup. +func (mr *MockRepositoryMockRecorder) GetCLAGroup(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroup", reflect.TypeOf((*MockRepository)(nil).GetCLAGroup), ctx, claGroupID) +} + +// GetCLAGroupNameByID mocks base method. +func (m *MockRepository) GetCLAGroupNameByID(ctx context.Context, claGroupID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCLAGroupNameByID", ctx, claGroupID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCLAGroupNameByID indicates an expected call of GetCLAGroupNameByID. +func (mr *MockRepositoryMockRecorder) GetCLAGroupNameByID(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupNameByID", reflect.TypeOf((*MockRepository)(nil).GetCLAGroupNameByID), ctx, claGroupID) +} + +// GetClaGroupIDForProject mocks base method. +func (m *MockRepository) GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*projects_cla_groups.ProjectClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClaGroupIDForProject", ctx, projectSFID) + ret0, _ := ret[0].(*projects_cla_groups.ProjectClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClaGroupIDForProject indicates an expected call of GetClaGroupIDForProject. +func (mr *MockRepositoryMockRecorder) GetClaGroupIDForProject(ctx, projectSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaGroupIDForProject", reflect.TypeOf((*MockRepository)(nil).GetClaGroupIDForProject), ctx, projectSFID) +} + +// GetProjectsIdsForAllFoundation mocks base method. +func (m *MockRepository) GetProjectsIdsForAllFoundation(ctx context.Context) ([]*projects_cla_groups.ProjectClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectsIdsForAllFoundation", ctx) + ret0, _ := ret[0].([]*projects_cla_groups.ProjectClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectsIdsForAllFoundation indicates an expected call of GetProjectsIdsForAllFoundation. +func (mr *MockRepositoryMockRecorder) GetProjectsIdsForAllFoundation(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectsIdsForAllFoundation", reflect.TypeOf((*MockRepository)(nil).GetProjectsIdsForAllFoundation), ctx) +} + +// GetProjectsIdsForClaGroup mocks base method. +func (m *MockRepository) GetProjectsIdsForClaGroup(ctx context.Context, claGroupID string) ([]*projects_cla_groups.ProjectClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectsIdsForClaGroup", ctx, claGroupID) + ret0, _ := ret[0].([]*projects_cla_groups.ProjectClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectsIdsForClaGroup indicates an expected call of GetProjectsIdsForClaGroup. +func (mr *MockRepositoryMockRecorder) GetProjectsIdsForClaGroup(ctx, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectsIdsForClaGroup", reflect.TypeOf((*MockRepository)(nil).GetProjectsIdsForClaGroup), ctx, claGroupID) +} + +// GetProjectsIdsForFoundation mocks base method. +func (m *MockRepository) GetProjectsIdsForFoundation(ctx context.Context, foundationSFID string) ([]*projects_cla_groups.ProjectClaGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProjectsIdsForFoundation", ctx, foundationSFID) + ret0, _ := ret[0].([]*projects_cla_groups.ProjectClaGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProjectsIdsForFoundation indicates an expected call of GetProjectsIdsForFoundation. +func (mr *MockRepositoryMockRecorder) GetProjectsIdsForFoundation(ctx, foundationSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectsIdsForFoundation", reflect.TypeOf((*MockRepository)(nil).GetProjectsIdsForFoundation), ctx, foundationSFID) +} + +// IsAssociated mocks base method. +func (m *MockRepository) IsAssociated(ctx context.Context, projectSFID, claGroupID string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsAssociated", ctx, projectSFID, claGroupID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsAssociated indicates an expected call of IsAssociated. +func (mr *MockRepositoryMockRecorder) IsAssociated(ctx, projectSFID, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAssociated", reflect.TypeOf((*MockRepository)(nil).IsAssociated), ctx, projectSFID, claGroupID) +} + +// IsExistingFoundationLevelCLAGroup mocks base method. +func (m *MockRepository) IsExistingFoundationLevelCLAGroup(ctx context.Context, foundationSFID string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsExistingFoundationLevelCLAGroup", ctx, foundationSFID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsExistingFoundationLevelCLAGroup indicates an expected call of IsExistingFoundationLevelCLAGroup. +func (mr *MockRepositoryMockRecorder) IsExistingFoundationLevelCLAGroup(ctx, foundationSFID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsExistingFoundationLevelCLAGroup", reflect.TypeOf((*MockRepository)(nil).IsExistingFoundationLevelCLAGroup), ctx, foundationSFID) +} + +// RemoveProjectAssociatedWithClaGroup mocks base method. +func (m *MockRepository) RemoveProjectAssociatedWithClaGroup(ctx context.Context, claGroupID string, projectSFIDList []string, all bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveProjectAssociatedWithClaGroup", ctx, claGroupID, projectSFIDList, all) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveProjectAssociatedWithClaGroup indicates an expected call of RemoveProjectAssociatedWithClaGroup. +func (mr *MockRepositoryMockRecorder) RemoveProjectAssociatedWithClaGroup(ctx, claGroupID, projectSFIDList, all interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProjectAssociatedWithClaGroup", reflect.TypeOf((*MockRepository)(nil).RemoveProjectAssociatedWithClaGroup), ctx, claGroupID, projectSFIDList, all) +} + +// UpdateClaGroupName mocks base method. +func (m *MockRepository) UpdateClaGroupName(ctx context.Context, projectSFID, claGroupName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateClaGroupName", ctx, projectSFID, claGroupName) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateClaGroupName indicates an expected call of UpdateClaGroupName. +func (mr *MockRepositoryMockRecorder) UpdateClaGroupName(ctx, projectSFID, claGroupName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClaGroupName", reflect.TypeOf((*MockRepository)(nil).UpdateClaGroupName), ctx, projectSFID, claGroupName) +} + +// UpdateRepositoriesCount mocks base method. +func (m *MockRepository) UpdateRepositoriesCount(ctx context.Context, projectSFID string, diff int64, reset bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRepositoriesCount", ctx, projectSFID, diff, reset) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateRepositoriesCount indicates an expected call of UpdateRepositoriesCount. +func (mr *MockRepositoryMockRecorder) UpdateRepositoriesCount(ctx, projectSFID, diff, reset interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepositoriesCount", reflect.TypeOf((*MockRepository)(nil).UpdateRepositoriesCount), ctx, projectSFID, diff, reset) +} diff --git a/cla-backend-go/projects_cla_groups/models.go b/cla-backend-go/projects_cla_groups/models.go index 98100ce47..0b7ccd4e5 100644 --- a/cla-backend-go/projects_cla_groups/models.go +++ b/cla-backend-go/projects_cla_groups/models.go @@ -13,7 +13,10 @@ type ProjectClaGroup struct { FoundationSFID string `dynamodbav:"foundation_sfid" json:"foundation_sfid"` FoundationName string `dynamodbav:"foundation_name" json:"foundation_name"` RepositoriesCount int64 `dynamodbav:"repositories_count" json:"repositories_count"` + Note string `dynamodbav:"version" json:"note"` Version string `dynamodbav:"version" json:"version"` + DateCreated string `dynamodbav:"version" json:"date_created"` + DateModified string `dynamodbav:"version" json:"date_modified"` } // Quick model to grab the bare minimum values diff --git a/cla-backend-go/projects_cla_groups/repository.go b/cla-backend-go/projects_cla_groups/repository.go index aa77848bd..55efa5b9d 100644 --- a/cla-backend-go/projects_cla_groups/repository.go +++ b/cla-backend-go/projects_cla_groups/repository.go @@ -4,6 +4,7 @@ package projects_cla_groups import ( + "context" "errors" "fmt" "strconv" @@ -42,18 +43,19 @@ var ( // Repository provides interface for interacting with project_cla_groups table type Repository interface { - GetClaGroupIDForProject(projectSFID string) (*ProjectClaGroup, error) - GetProjectsIdsForClaGroup(claGroupID string) ([]*ProjectClaGroup, error) - GetProjectsIdsForFoundation(foundationSFID string) ([]*ProjectClaGroup, error) - GetProjectsIdsForAllFoundation() ([]*ProjectClaGroup, error) - AssociateClaGroupWithProject(claGroupID string, projectSFID string, foundationSFID string) error - RemoveProjectAssociatedWithClaGroup(claGroupID string, projectSFIDList []string, all bool) error - GetCLAGroupNameByID(claGroupID string) (string, error) - GetCLAGroup(claGroupID string) (*ProjectClaGroup, error) - - IsExistingFoundationLevelCLAGroup(foundationSFID string) (bool, error) - IsAssociated(projectSFID string, claGroupID string) (bool, error) - UpdateRepositoriesCount(projectSFID string, diff int64, reset bool) error + GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*ProjectClaGroup, error) + GetProjectsIdsForClaGroup(ctx context.Context, claGroupID string) ([]*ProjectClaGroup, error) + GetProjectsIdsForFoundation(ctx context.Context, foundationSFID string) ([]*ProjectClaGroup, error) + GetProjectsIdsForAllFoundation(ctx context.Context) ([]*ProjectClaGroup, error) + AssociateClaGroupWithProject(ctx context.Context, claGroupID string, projectSFID string, foundationSFID string) error + RemoveProjectAssociatedWithClaGroup(ctx context.Context, claGroupID string, projectSFIDList []string, all bool) error + GetCLAGroupNameByID(ctx context.Context, claGroupID string) (string, error) + GetCLAGroup(ctx context.Context, claGroupID string) (*ProjectClaGroup, error) + + IsExistingFoundationLevelCLAGroup(ctx context.Context, foundationSFID string) (bool, error) + IsAssociated(ctx context.Context, projectSFID string, claGroupID string) (bool, error) + UpdateRepositoriesCount(ctx context.Context, projectSFID string, diff int64, reset bool) error + UpdateClaGroupName(ctx context.Context, projectSFID string, claGroupName string) error } type repo struct { @@ -71,14 +73,14 @@ func NewRepository(awsSession *session.Session, stage string) Repository { } } -func (repo *repo) queryClaGroupsProjects(keyCondition expression.KeyConditionBuilder, indexName *string) ([]*ProjectClaGroup, error) { +func (repo *repo) queryClaGroupsProjects(ctx context.Context, keyCondition expression.KeyConditionBuilder, indexName *string) ([]*ProjectClaGroup, error) { f := logrus.Fields{ - "functionName": "queryClaGroupsProjects", - "indexName": aws.StringValue(indexName), - "keyCondition": fmt.Sprintf("%+v", keyCondition), + "functionName": "project_cla_groups.repository.queryClaGroupsProjects", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "indexName": aws.StringValue(indexName), + "keyCondition": fmt.Sprintf("%+v", keyCondition), } - log.WithFields(f).Debug("building query...") expr, err := expression.NewBuilder().WithKeyCondition(keyCondition).Build() if err != nil { log.WithFields(f).Warnf("error building expression for project cla groups, error: %v", err) @@ -96,7 +98,7 @@ func (repo *repo) queryClaGroupsProjects(keyCondition expression.KeyConditionBui var projectClaGroups []*ProjectClaGroup for { - log.WithFields(f).Debugf("running query using input: %+v", queryInput) + // log.WithFields(f).Debugf("running query using input: %+v", queryInput) results, errQuery := repo.dynamoDBClient.Query(queryInput) if errQuery != nil { log.WithFields(f).Warnf("error retrieving project cla-groups, error: %v", errQuery) @@ -123,11 +125,12 @@ func (repo *repo) queryClaGroupsProjects(keyCondition expression.KeyConditionBui } // GetClaGroupIDForProject retrieves the CLA Group ID for the project -func (repo *repo) GetClaGroupIDForProject(projectSFID string) (*ProjectClaGroup, error) { +func (repo *repo) GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*ProjectClaGroup, error) { f := logrus.Fields{ - "functionName": "GetClaGroupIDForProject", - "tableName": repo.tableName, - "projectSFID": projectSFID, + "functionName": "project_cla_groups.repository.GetClaGroupIDForProject", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "tableName": repo.tableName, + "projectSFID": projectSFID, } result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ @@ -147,7 +150,7 @@ func (repo *repo) GetClaGroupIDForProject(projectSFID string) (*ProjectClaGroup, if len(result.Item) == 0 { // Query by foundation sfid index returns multiple results log.WithFields(f).Debug("no results querying by project SFID - checking if this is a foundation SFID") - pcgs, foundationErr := repo.GetProjectsIdsForFoundation(projectSFID) + pcgs, foundationErr := repo.GetProjectsIdsForFoundation(ctx, projectSFID) if foundationErr != nil { log.WithFields(f).Warnf("unable to lookup CLA Group associated with project, error: %+v", foundationErr) return nil, err @@ -171,18 +174,23 @@ func (repo *repo) GetClaGroupIDForProject(projectSFID string) (*ProjectClaGroup, return &out, nil } -func (repo *repo) GetProjectsIdsForClaGroup(claGroupID string) ([]*ProjectClaGroup, error) { +func (repo *repo) GetProjectsIdsForClaGroup(ctx context.Context, claGroupID string) ([]*ProjectClaGroup, error) { keyCondition := expression.Key("cla_group_id").Equal(expression.Value(claGroupID)) - return repo.queryClaGroupsProjects(keyCondition, aws.String(CLAGroupIDIndex)) + return repo.queryClaGroupsProjects(ctx, keyCondition, aws.String(CLAGroupIDIndex)) } -func (repo *repo) GetProjectsIdsForFoundation(foundationSFID string) ([]*ProjectClaGroup, error) { +func (repo *repo) GetProjectsIdsForFoundation(ctx context.Context, foundationSFID string) ([]*ProjectClaGroup, error) { keyCondition := expression.Key("foundation_sfid").Equal(expression.Value(foundationSFID)) - return repo.queryClaGroupsProjects(keyCondition, aws.String(FoundationSFIDIndex)) + return repo.queryClaGroupsProjects(ctx, keyCondition, aws.String(FoundationSFIDIndex)) } -func (repo *repo) GetProjectsIdsForAllFoundation() ([]*ProjectClaGroup, error) { - f := logrus.Fields{"functionName": "GetProjectsIdsForAllFoundation", "tableName": repo.tableName} +func (repo *repo) GetProjectsIdsForAllFoundation(ctx context.Context) ([]*ProjectClaGroup, error) { + f := logrus.Fields{ + "functionName": "project_cla_groups.repository.GetProjectsIdsForAllFoundation", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "tableName": repo.tableName, + } + scanInput := &dynamodb.ScanInput{ TableName: aws.String(repo.tableName), } @@ -210,9 +218,10 @@ func (repo *repo) GetProjectsIdsForAllFoundation() ([]*ProjectClaGroup, error) { } // AssociateClaGroupWithProject creates entry in db to track cla_group association with project/foundation -func (repo *repo) AssociateClaGroupWithProject(claGroupID string, projectSFID string, foundationSFID string) error { +func (repo *repo) AssociateClaGroupWithProject(ctx context.Context, claGroupID string, projectSFID string, foundationSFID string) error { f := logrus.Fields{ - "functionName": "AssociateClaGroupWithProject", + "functionName": "project_cla_groups.repository.AssociateClaGroupWithProject", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "projectSFID": projectSFID, "foundationSFID": foundationSFID, @@ -240,30 +249,49 @@ func (repo *repo) AssociateClaGroupWithProject(claGroupID string, projectSFID st } // Lookup the CLA Group name/Project Name - claGroupName, claGroupLookupErr := repo.GetCLAGroupNameByID(claGroupID) + claGroupName, claGroupLookupErr := repo.GetCLAGroupNameByID(ctx, claGroupID) if claGroupLookupErr != nil { claGroupName = NotDefined log.Warnf("unable to lookup CLA Group/Project by ID, error: %+v - using '%s'", claGroupLookupErr, NotDefined) } - input := &ProjectClaGroup{ - ProjectSFID: projectSFID, - ProjectName: projectName, - ClaGroupID: claGroupID, - ClaGroupName: claGroupName, - FoundationSFID: foundationSFID, - FoundationName: foundationName, - Version: "v1", - } - - av, err := dynamodbattribute.MarshalMap(input) - if err != nil { - return err + _, nowStr := utils.CurrentTime() + item := map[string]*dynamodb.AttributeValue{ + "project_sfid": { + S: aws.String(projectSFID), + }, + "project_name": { + S: aws.String(projectName), + }, + "cla_group_id": { + S: aws.String(claGroupID), + }, + "cla_group_name": { + S: aws.String(claGroupName), + }, + "foundation_sfid": { + S: aws.String(foundationSFID), + }, + "foundation_name": { + S: aws.String(foundationName), + }, + "note": { + S: aws.String(fmt.Sprintf("Associate CLA Group with project API request on: %s", nowStr)), + }, + "version": { + S: aws.String("v1"), + }, + "date_created": { + S: aws.String(nowStr), + }, + "date_modified": { + S: aws.String(nowStr), + }, } log.WithFields(f).Debug("Locating records with matching projectSFID...") - existingRecord, lookupErr := repo.GetClaGroupIDForProject(projectSFID) + existingRecord, lookupErr := repo.GetClaGroupIDForProject(ctx, projectSFID) if lookupErr != nil { log.WithFields(f).Warnf("cannot lookup record by projectSFID, error: %+v", lookupErr) } @@ -273,9 +301,9 @@ func (repo *repo) AssociateClaGroupWithProject(claGroupID string, projectSFID st log.WithFields(f).Debugf("record found with matching projectSFID: %+v", existingRecord) } - log.WithFields(f).Debugf("adding entry into the %s table with: %+v", repo.tableName, input) - _, err = repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ - Item: av, + log.WithFields(f).Debugf("adding entry into the %s table with: %+v", repo.tableName, item) + _, err := repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: item, TableName: aws.String(repo.tableName), ConditionExpression: aws.String("attribute_not_exists(project_sfid)"), }) @@ -294,14 +322,15 @@ func (repo *repo) AssociateClaGroupWithProject(claGroupID string, projectSFID st } // RemoveProjectAssociatedWithClaGroup removes all associated project with cla_group -func (repo *repo) RemoveProjectAssociatedWithClaGroup(claGroupID string, projectSFIDList []string, all bool) error { +func (repo *repo) RemoveProjectAssociatedWithClaGroup(ctx context.Context, claGroupID string, projectSFIDList []string, all bool) error { f := logrus.Fields{ - "functionName": "RemoveProjectAssociatedWithClaGroup", + "functionName": "project_cla_groups.repository.RemoveProjectAssociatedWithClaGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "projectSFIDList": projectSFIDList, "all": all, } - list, err := repo.GetProjectsIdsForClaGroup(claGroupID) + list, err := repo.GetProjectsIdsForClaGroup(ctx, claGroupID) if err != nil { log.WithFields(f).Warnf("unable to fetch projects IDs for CLA Group, error: %+v", err) return err @@ -336,7 +365,7 @@ func (repo *repo) RemoveProjectAssociatedWithClaGroup(claGroupID string, project } // GetCLAGroupNameByID helper function to fetch the CLA Group name -func (repo *repo) GetCLAGroupNameByID(claGroupID string) (string, error) { +func (repo *repo) GetCLAGroupNameByID(ctx context.Context, claGroupID string) (string, error) { tableName := fmt.Sprintf("cla-%s-projects", repo.stage) result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ TableName: aws.String(tableName), @@ -363,7 +392,7 @@ func (repo *repo) GetCLAGroupNameByID(claGroupID string) (string, error) { } // GetCLAGroup helper function to fetch the CLA Group -func (repo *repo) GetCLAGroup(claGroupID string) (*ProjectClaGroup, error) { +func (repo *repo) GetCLAGroup(ctx context.Context, claGroupID string) (*ProjectClaGroup, error) { tableName := fmt.Sprintf("cla-%s-projects", repo.stage) result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ TableName: aws.String(tableName), @@ -390,16 +419,17 @@ func (repo *repo) GetCLAGroup(claGroupID string) (*ProjectClaGroup, error) { } // UpdateRepositoriesCount updates the repositories count -func (repo *repo) UpdateRepositoriesCount(projectSFID string, diff int64, reset bool) error { +func (repo *repo) UpdateRepositoriesCount(ctx context.Context, projectSFID string, diff int64, reset bool) error { f := logrus.Fields{ - "functionName": "UpdateRepositoriesCount", - "projectSFID": projectSFID, - "diff": diff, - "reset": reset, + "functionName": "project_cla_groups.repository.UpdateRepositoriesCount", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "diff": diff, + "reset": reset, } // Check to see if we have an existing record - existingProjectCLAGroupMapping, err := repo.GetClaGroupIDForProject(projectSFID) + existingProjectCLAGroupMapping, err := repo.GetClaGroupIDForProject(ctx, projectSFID) if err != nil { log.WithFields(f).WithError(err).Warn("unable to lookup existing project cla group mapping") return err @@ -452,11 +482,66 @@ func (repo *repo) UpdateRepositoriesCount(projectSFID string, diff int64, reset return updateErr } +// UpdateClaGroupName updates cla group name for given projectSFID +func (repo *repo) UpdateClaGroupName(ctx context.Context, projectSFID string, claGroupName string) error { + f := logrus.Fields{ + "functionName": "project_cla_groups.repository.UpdateClaGroupName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "claGroupName": claGroupName, + } + + // Check to see if we have an existing record + existingProjectCLAGroupMapping, err := repo.GetClaGroupIDForProject(ctx, projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup existing project cla group mapping") + return err + } + if existingProjectCLAGroupMapping == nil { + log.WithFields(f).Warn("unable to lookup existing project cla group mapping - response is empty") + return &utils.ProjectCLAGroupMappingNotFound{ + ProjectSFID: projectSFID, + CLAGroupID: "", + Err: nil, + } + } + + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + var updateExpression string + + // update repositories_count based on reset flag + expressionAttributeNames["#N"] = aws.String("cla_group_name") + expressionAttributeValues[":n"] = &dynamodb.AttributeValue{S: &claGroupName} + updateExpression = "SET #N = :n" + + _, now := utils.CurrentTime() + expressionAttributeNames["#M"] = aws.String("date_modified") + expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(now)} + updateExpression = updateExpression + ", #M = :m" + + _, updateErr := repo.dynamoDBClient.UpdateItem(&dynamodb.UpdateItemInput{ + UpdateExpression: aws.String(updateExpression), + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + Key: map[string]*dynamodb.AttributeValue{ + "project_sfid": {S: aws.String(projectSFID)}, + }, + TableName: aws.String(repo.tableName), + }) + + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warn("update cla group name failed") + } + + return updateErr +} + // IsExistingFoundationLevelCLAGroup is a query helper function to determine if the // specified foundation SFID has an entry in the mapping table to signify that // it's a foundation level CLA Group (foundationSFID == projectSFID) -func (repo *repo) IsExistingFoundationLevelCLAGroup(foundationSFID string) (bool, error) { - projectCLAGroupModels, err := repo.GetProjectsIdsForFoundation(foundationSFID) +func (repo *repo) IsExistingFoundationLevelCLAGroup(ctx context.Context, foundationSFID string) (bool, error) { + projectCLAGroupModels, err := repo.GetProjectsIdsForFoundation(ctx, foundationSFID) if err != nil { return false, err } @@ -470,8 +555,8 @@ func (repo *repo) IsExistingFoundationLevelCLAGroup(foundationSFID string) (bool return false, nil } -func (repo *repo) IsAssociated(projectSFID string, claGroupID string) (bool, error) { - pmlist, err := repo.GetProjectsIdsForClaGroup(claGroupID) +func (repo *repo) IsAssociated(ctx context.Context, projectSFID string, claGroupID string) (bool, error) { + pmlist, err := repo.GetProjectsIdsForClaGroup(ctx, claGroupID) if err != nil { return false, err } diff --git a/cla-backend-go/projects_cla_groups/service.go b/cla-backend-go/projects_cla_groups/service.go new file mode 100644 index 000000000..d98f53b7f --- /dev/null +++ b/cla-backend-go/projects_cla_groups/service.go @@ -0,0 +1,95 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package projects_cla_groups + +import "context" + +// ProjectCLAGroupsService interface +type ProjectCLAGroupsService interface { + GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*ProjectClaGroup, error) + GetProjectsIdsForClaGroup(ctx context.Context, claGroupID string) ([]*ProjectClaGroup, error) + GetProjectsIdsForFoundation(ctx context.Context, foundationSFID string) ([]*ProjectClaGroup, error) + GetProjectsIdsForAllFoundation() ([]*ProjectClaGroup, error) + AssociateClaGroupWithProject(ctx context.Context, claGroupID string, projectSFID string, foundationSFID string) error + RemoveProjectAssociatedWithClaGroup(ctx context.Context, claGroupID string, projectSFIDList []string, all bool) error + GetCLAGroupNameByID(ctx context.Context, claGroupID string) (string, error) + GetCLAGroup(ctx context.Context, claGroupID string) (*ProjectClaGroup, error) + + IsExistingFoundationLevelCLAGroup(ctx context.Context, foundationSFID string) (bool, error) + IsAssociated(ctx context.Context, projectSFID string, claGroupID string) (bool, error) + UpdateRepositoriesCount(ctx context.Context, projectSFID string, diff int64, reset bool) error + UpdateClaGroupName(ctx context.Context, projectSFID string, claGroupName string) error +} + +// Service model +type Service struct { + repo Repository +} + +// NewService creates a new whitelist service +func NewService(repo Repository) Service { + return Service{ + repo, + } +} + +// GetClaGroupIDForProject service method +func (s Service) GetClaGroupIDForProject(ctx context.Context, projectSFID string) (*ProjectClaGroup, error) { + return s.repo.GetClaGroupIDForProject(ctx, projectSFID) +} + +// GetProjectsIdsForClaGroup service method +func (s Service) GetProjectsIdsForClaGroup(ctx context.Context, claGroupID string) ([]*ProjectClaGroup, error) { + return s.repo.GetProjectsIdsForClaGroup(ctx, claGroupID) +} + +// GetProjectsIdsForFoundation service method +func (s Service) GetProjectsIdsForFoundation(ctx context.Context, foundationSFID string) ([]*ProjectClaGroup, error) { + return s.repo.GetProjectsIdsForFoundation(ctx, foundationSFID) +} + +// GetProjectsIdsForAllFoundation service method +func (s Service) GetProjectsIdsForAllFoundation(ctx context.Context) ([]*ProjectClaGroup, error) { + return s.repo.GetProjectsIdsForAllFoundation(ctx) +} + +// AssociateClaGroupWithProject service method +func (s Service) AssociateClaGroupWithProject(ctx context.Context, claGroupID string, projectSFID string, foundationSFID string) error { + return s.repo.AssociateClaGroupWithProject(ctx, claGroupID, projectSFID, foundationSFID) +} + +// RemoveProjectAssociatedWithClaGroup service method +func (s Service) RemoveProjectAssociatedWithClaGroup(ctx context.Context, claGroupID string, projectSFIDList []string, all bool) error { + return s.repo.RemoveProjectAssociatedWithClaGroup(ctx, claGroupID, projectSFIDList, all) +} + +// GetCLAGroupNameByID service method +func (s Service) GetCLAGroupNameByID(ctx context.Context, claGroupID string) (string, error) { + return s.repo.GetCLAGroupNameByID(ctx, claGroupID) +} + +// GetCLAGroup service method +func (s Service) GetCLAGroup(ctx context.Context, claGroupID string) (*ProjectClaGroup, error) { + return s.repo.GetCLAGroup(ctx, claGroupID) +} + +// IsExistingFoundationLevelCLAGroup service method +func (s Service) IsExistingFoundationLevelCLAGroup(ctx context.Context, foundationSFID string) (bool, error) { + return s.repo.IsExistingFoundationLevelCLAGroup(ctx, foundationSFID) +} + +// IsAssociated service method +func (s Service) IsAssociated(ctx context.Context, projectSFID string, claGroupID string) (bool, error) { + return s.repo.IsAssociated(ctx, projectSFID, claGroupID) +} + +// UpdateRepositoriesCount service method +func (s Service) UpdateRepositoriesCount(ctx context.Context, projectSFID string, diff int64, reset bool) error { + return s.repo.UpdateRepositoriesCount(ctx, projectSFID, diff, reset) +} + +// UpdateClaGroupName service method +func (s Service) UpdateClaGroupName(ctx context.Context, projectSFID string, claGroupName string) error { + return s.repo.UpdateClaGroupName(ctx, projectSFID, claGroupName) +} diff --git a/cla-backend-go/repositories/constants.go b/cla-backend-go/repositories/constants.go new file mode 100644 index 000000000..c673e5661 --- /dev/null +++ b/cla-backend-go/repositories/constants.go @@ -0,0 +1,61 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package repositories + +// RepositoryIDColumn constant +const RepositoryIDColumn = "repository_id" + +// RepositoryNameColumn constant +const RepositoryNameColumn = "repository_name" + +// RepositoryTypeColumn constant +const RepositoryTypeColumn = "repository_type" + +// RepositoryExternalIDColumn constant +const RepositoryExternalIDColumn = "repository_external_id" + +// RepositoryProjectIDColumn constant +const RepositoryProjectIDColumn = "project_sfid" + +// RepositoryCLAGroupIDColumn constant +const RepositoryCLAGroupIDColumn = "repository_project_id" + +// RepositoryOrganizationNameColumn constant +const RepositoryOrganizationNameColumn = "repository_organization_name" + +// RepositoryEnabledColumn constant +const RepositoryEnabledColumn = "enabled" + +// RepositoryNoteColumn constant +const RepositoryNoteColumn = "note" + +// RepositoryDateModifiedColumn constant +const RepositoryDateModifiedColumn = "date_modified" + +// RepositoryEnabled constant +const RepositoryEnabled = "enabled" + +// RepositoryDisabled constant +const RepositoryDisabled = "disabled" + +// RepositoryProjectIndex constant +const RepositoryProjectIndex = "project-repository-index" + +// RepositoryTypeIndex constant +const RepositoryTypeIndex = "repository-type-index" + +// RepositoryExternalIDIndex constant +const RepositoryExternalIDIndex = "external-repository-index" + +// RepositoryProjectSFIDIndex constant +const RepositoryProjectSFIDIndex = "project-sfid-repository-index" + +// RepositoryProjectSFIDOrganizationNameIndex constant +const RepositoryProjectSFIDOrganizationNameIndex = "project-sfid-repository-organization-name-index" + +// RepositoryOrganizationNameIndex constant +const RepositoryOrganizationNameIndex = "repository-organization-name-index" + +// RepositoryNameIndex constant +const RepositoryNameIndex = "repository-name-index" diff --git a/cla-backend-go/repositories/handlers.go b/cla-backend-go/repositories/handlers.go index 7fb6f64d6..547579555 100644 --- a/cla-backend-go/repositories/handlers.go +++ b/cla-backend-go/repositories/handlers.go @@ -8,9 +8,9 @@ import ( "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/github_repositories" + "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/github_repositories" "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/go-openapi/runtime/middleware" @@ -25,7 +25,7 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv if !claUser.IsAuthorizedForProject(params.ProjectSFID) { return github_repositories.NewGetProjectGithubRepositoriesForbidden().WithPayload(&models.ErrorResponse{ Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Add GitHub Repository with Project scope of %s", + Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Add GitHub CombinedRepository with Project scope of %s", claUser.LFUsername, params.ProjectSFID), }) } @@ -44,7 +44,7 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv if !claUser.IsAuthorizedForProject(params.ProjectSFID) { return github_repositories.NewAddProjectGithubRepositoryForbidden().WithPayload(&models.ErrorResponse{ Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Add GitHub Repository with Project scope of %s", + Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Add GitHub CombinedRepository with Project scope of %s", claUser.LFUsername, params.ProjectSFID), }) } @@ -52,12 +52,12 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv if err != nil { return github_repositories.NewAddProjectGithubRepositoryBadRequest().WithPayload(errorResponse(err)) } - eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryAdded, - ProjectID: utils.StringValue(params.GithubRepositoryInput.RepositoryProjectID), - ExternalProjectID: params.ProjectSFID, - UserID: claUser.UserID, - LfUsername: claUser.LFUsername, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryAdded, + CLAGroupID: utils.StringValue(params.GithubRepositoryInput.RepositoryProjectID), + ProjectSFID: params.ProjectSFID, + UserID: claUser.UserID, + LfUsername: claUser.LFUsername, UserModel: &models.User{ Username: claUser.LFUsername, }, @@ -75,13 +75,13 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv if !claUser.IsAuthorizedForProject(params.ProjectSFID) { return github_repositories.NewDeleteProjectGithubRepositoryForbidden().WithPayload(&models.ErrorResponse{ Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Delete GitHub Repository with Project scope of %s", + Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Delete GitHub CombinedRepository with Project scope of %s", claUser.LFUsername, params.ProjectSFID), }) } ghRepo, err := service.GetRepository(ctx, params.RepositoryID) if err != nil { - if err == ErrGithubRepositoryNotFound { + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { return github_repositories.NewDeleteProjectGithubRepositoryNotFound() } return github_repositories.NewDeleteProjectGithubRepositoryBadRequest().WithPayload(errorResponse(err)) @@ -90,12 +90,11 @@ func Configure(api *operations.ClaAPI, service Service, eventService events.Serv if err != nil { return github_repositories.NewDeleteProjectGithubRepositoryBadRequest().WithPayload(errorResponse(err)) } - eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryDisabled, - ExternalProjectID: params.ProjectSFID, - ProjectID: ghRepo.RepositoryProjectID, - UserID: claUser.UserID, - LfUsername: claUser.LFUsername, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryDisabled, + ProjectSFID: params.ProjectSFID, + UserID: claUser.UserID, + LfUsername: claUser.LFUsername, EventData: &events.RepositoryDisabledEventData{ RepositoryName: ghRepo.RepositoryName, }, diff --git a/cla-backend-go/repositories/mock/mock_repository.go b/cla-backend-go/repositories/mock/mock_repository.go index 07bca37b7..228ea8e85 100644 --- a/cla-backend-go/repositories/mock/mock_repository.go +++ b/cla-backend-go/repositories/mock/mock_repository.go @@ -12,204 +12,282 @@ import ( context "context" reflect "reflect" - models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" gomock "github.com/golang/mock/gomock" ) -// MockRepository is a mock of Repository interface -type MockRepository struct { +// MockRepositoryInterface is a mock of RepositoryInterface interface. +type MockRepositoryInterface struct { ctrl *gomock.Controller - recorder *MockRepositoryMockRecorder + recorder *MockRepositoryInterfaceMockRecorder } -// MockRepositoryMockRecorder is the mock recorder for MockRepository -type MockRepositoryMockRecorder struct { - mock *MockRepository +// GitHubDisableRepositoriesOfOrganizationParent implements repositories.RepositoryInterface. +func (*MockRepositoryInterface) GitHubDisableRepositoriesOfOrganizationParent(ctx context.Context, parentProjectSFID string, githubOrgName string) error { + panic("unimplemented") } -// NewMockRepository creates a new mock instance -func NewMockRepository(ctrl *gomock.Controller) *MockRepository { - mock := &MockRepository{ctrl: ctrl} - mock.recorder = &MockRepositoryMockRecorder{mock} +// MockRepositoryInterfaceMockRecorder is the mock recorder for MockRepositoryInterface. +type MockRepositoryInterfaceMockRecorder struct { + mock *MockRepositoryInterface +} + +// NewMockRepositoryInterface creates a new mock instance. +func NewMockRepositoryInterface(ctrl *gomock.Controller) *MockRepositoryInterface { + mock := &MockRepositoryInterface{ctrl: ctrl} + mock.recorder = &MockRepositoryInterfaceMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryInterface) EXPECT() *MockRepositoryInterfaceMockRecorder { return m.recorder } -// AddGithubRepository mocks base method -func (m *MockRepository) AddGithubRepository(ctx context.Context, externalProjectID, projectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { +// GitHubAddRepository mocks base method. +func (m *MockRepositoryInterface) GitHubAddRepository(ctx context.Context, externalProjectID, projectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddGithubRepository", ctx, externalProjectID, projectSFID, input) + ret := m.ctrl.Call(m, "GitHubAddRepository", ctx, externalProjectID, projectSFID, input) ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// AddGithubRepository indicates an expected call of AddGithubRepository -func (mr *MockRepositoryMockRecorder) AddGithubRepository(ctx, externalProjectID, projectSFID, input interface{}) *gomock.Call { +// GitHubAddRepository indicates an expected call of GitHubAddRepository. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubAddRepository(ctx, externalProjectID, projectSFID, input interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGithubRepository", reflect.TypeOf((*MockRepository)(nil).AddGithubRepository), ctx, externalProjectID, projectSFID, input) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubAddRepository", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubAddRepository), ctx, externalProjectID, projectSFID, input) } -// UpdateClaGroupID mocks base method -func (m *MockRepository) UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { +// GitHubDisableRepositoriesByProjectID mocks base method. +func (m *MockRepositoryInterface) GitHubDisableRepositoriesByProjectID(ctx context.Context, projectID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateClaGroupID", ctx, repositoryID, claGroupID) + ret := m.ctrl.Call(m, "GitHubDisableRepositoriesByProjectID", ctx, projectID) ret0, _ := ret[0].(error) return ret0 } -// UpdateClaGroupID indicates an expected call of UpdateClaGroupID -func (mr *MockRepositoryMockRecorder) UpdateClaGroupID(ctx, repositoryID, claGroupID interface{}) *gomock.Call { +// GitHubDisableRepositoriesByProjectID indicates an expected call of GitHubDisableRepositoriesByProjectID. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubDisableRepositoriesByProjectID(ctx, projectID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClaGroupID", reflect.TypeOf((*MockRepository)(nil).UpdateClaGroupID), ctx, repositoryID, claGroupID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubDisableRepositoriesByProjectID", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubDisableRepositoriesByProjectID), ctx, projectID) } -// EnableRepository mocks base method -func (m *MockRepository) EnableRepository(ctx context.Context, repositoryID string) error { +// GitHubDisableRepositoriesOfOrganization mocks base method. +func (m *MockRepositoryInterface) GitHubDisableRepositoriesOfOrganization(ctx context.Context, externalProjectID, githubOrgName string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnableRepository", ctx, repositoryID) + ret := m.ctrl.Call(m, "GitHubDisableRepositoriesOfOrganization", ctx, externalProjectID, githubOrgName) ret0, _ := ret[0].(error) return ret0 } -// EnableRepository indicates an expected call of EnableRepository -func (mr *MockRepositoryMockRecorder) EnableRepository(ctx, repositoryID interface{}) *gomock.Call { +// GitHubDisableRepositoriesOfOrganization indicates an expected call of GitHubDisableRepositoriesOfOrganization. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubDisableRepositoriesOfOrganization(ctx, externalProjectID, githubOrgName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableRepository", reflect.TypeOf((*MockRepository)(nil).EnableRepository), ctx, repositoryID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubDisableRepositoriesOfOrganization", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubDisableRepositoriesOfOrganization), ctx, externalProjectID, githubOrgName) } -// DisableRepository mocks base method -func (m *MockRepository) DisableRepository(ctx context.Context, repositoryID string) error { +// GitHubDisableRepository mocks base method. +func (m *MockRepositoryInterface) GitHubDisableRepository(ctx context.Context, repositoryID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisableRepository", ctx, repositoryID) + ret := m.ctrl.Call(m, "GitHubDisableRepository", ctx, repositoryID) ret0, _ := ret[0].(error) return ret0 } -// DisableRepository indicates an expected call of DisableRepository -func (mr *MockRepositoryMockRecorder) DisableRepository(ctx, repositoryID interface{}) *gomock.Call { +// GitHubDisableRepository indicates an expected call of GitHubDisableRepository. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubDisableRepository(ctx, repositoryID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepository", reflect.TypeOf((*MockRepository)(nil).DisableRepository), ctx, repositoryID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubDisableRepository", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubDisableRepository), ctx, repositoryID) } -// DisableRepositoriesByProjectID mocks base method -func (m *MockRepository) DisableRepositoriesByProjectID(ctx context.Context, projectID string) error { +// GitHubEnableRepository mocks base method. +func (m *MockRepositoryInterface) GitHubEnableRepository(ctx context.Context, repositoryID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisableRepositoriesByProjectID", ctx, projectID) + ret := m.ctrl.Call(m, "GitHubEnableRepository", ctx, repositoryID) ret0, _ := ret[0].(error) return ret0 } -// DisableRepositoriesByProjectID indicates an expected call of DisableRepositoriesByProjectID -func (mr *MockRepositoryMockRecorder) DisableRepositoriesByProjectID(ctx, projectID interface{}) *gomock.Call { +// GitHubEnableRepository indicates an expected call of GitHubEnableRepository. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubEnableRepository(ctx, repositoryID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepositoriesByProjectID", reflect.TypeOf((*MockRepository)(nil).DisableRepositoriesByProjectID), ctx, projectID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubEnableRepository", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubEnableRepository), ctx, repositoryID) } -// DisableRepositoriesOfGithubOrganization mocks base method -func (m *MockRepository) DisableRepositoriesOfGithubOrganization(ctx context.Context, externalProjectID, githubOrgName string) error { +// GitHubEnableRepositoryWithCLAGroupID mocks base method. +func (m *MockRepositoryInterface) GitHubEnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisableRepositoriesOfGithubOrganization", ctx, externalProjectID, githubOrgName) + ret := m.ctrl.Call(m, "GitHubEnableRepositoryWithCLAGroupID", ctx, repositoryID, claGroupID) ret0, _ := ret[0].(error) return ret0 } -// DisableRepositoriesOfGithubOrganization indicates an expected call of DisableRepositoriesOfGithubOrganization -func (mr *MockRepositoryMockRecorder) DisableRepositoriesOfGithubOrganization(ctx, externalProjectID, githubOrgName interface{}) *gomock.Call { +// GitHubEnableRepositoryWithCLAGroupID indicates an expected call of GitHubEnableRepositoryWithCLAGroupID. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubEnableRepositoryWithCLAGroupID(ctx, repositoryID, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubEnableRepositoryWithCLAGroupID", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubEnableRepositoryWithCLAGroupID), ctx, repositoryID, claGroupID) +} + +// GitHubGetCLAGroupRepositoriesGroupByOrgs mocks base method. +func (m *MockRepositoryInterface) GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepositoriesGroupByOrgs, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GitHubGetCLAGroupRepositoriesGroupByOrgs", ctx, projectID, enabled) + ret0, _ := ret[0].([]*models.GithubRepositoriesGroupByOrgs) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GitHubGetCLAGroupRepositoriesGroupByOrgs indicates an expected call of GitHubGetCLAGroupRepositoriesGroupByOrgs. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx, projectID, enabled interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetCLAGroupRepositoriesGroupByOrgs", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetCLAGroupRepositoriesGroupByOrgs), ctx, projectID, enabled) +} + +// GitHubGetRepositoriesByCLAGroup mocks base method. +func (m *MockRepositoryInterface) GitHubGetRepositoriesByCLAGroup(ctx context.Context, claGroup string, enabled bool) ([]*models.GithubRepository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GitHubGetRepositoriesByCLAGroup", ctx, claGroup, enabled) + ret0, _ := ret[0].([]*models.GithubRepository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GitHubGetRepositoriesByCLAGroup indicates an expected call of GitHubGetRepositoriesByCLAGroup. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetRepositoriesByCLAGroup(ctx, claGroup, enabled interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepositoriesOfGithubOrganization", reflect.TypeOf((*MockRepository)(nil).DisableRepositoriesOfGithubOrganization), ctx, externalProjectID, githubOrgName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetRepositoriesByCLAGroup", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetRepositoriesByCLAGroup), ctx, claGroup, enabled) } -// GetRepository mocks base method -func (m *MockRepository) GetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) { +// GitHubGetRepositoriesByOrganizationName mocks base method. +func (m *MockRepositoryInterface) GitHubGetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepository", ctx, repositoryID) + ret := m.ctrl.Call(m, "GitHubGetRepositoriesByOrganizationName", ctx, gitHubOrgName) + ret0, _ := ret[0].([]*models.GithubRepository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GitHubGetRepositoriesByOrganizationName indicates an expected call of GitHubGetRepositoriesByOrganizationName. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetRepositoriesByOrganizationName(ctx, gitHubOrgName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetRepositoriesByOrganizationName", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetRepositoriesByOrganizationName), ctx, gitHubOrgName) +} + +// GitHubGetRepository mocks base method. +func (m *MockRepositoryInterface) GitHubGetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GitHubGetRepository", ctx, repositoryID) ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepository indicates an expected call of GetRepository -func (mr *MockRepositoryMockRecorder) GetRepository(ctx, repositoryID interface{}) *gomock.Call { +// GitHubGetRepository indicates an expected call of GitHubGetRepository. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetRepository(ctx, repositoryID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepository", reflect.TypeOf((*MockRepository)(nil).GetRepository), ctx, repositoryID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetRepository", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetRepository), ctx, repositoryID) } -// GetRepositoryByGithubID mocks base method -func (m *MockRepository) GetRepositoryByGithubID(ctx context.Context, externalID string, enabled bool) (*models.GithubRepository, error) { +// GitHubGetRepositoryByExternalID mocks base method. +func (m *MockRepositoryInterface) GitHubGetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoryByGithubID", ctx, externalID, enabled) + ret := m.ctrl.Call(m, "GitHubGetRepositoryByExternalID", ctx, repositoryExternalID) ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepositoryByGithubID indicates an expected call of GetRepositoryByGithubID -func (mr *MockRepositoryMockRecorder) GetRepositoryByGithubID(ctx, externalID, enabled interface{}) *gomock.Call { +// GitHubGetRepositoryByExternalID indicates an expected call of GitHubGetRepositoryByExternalID. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetRepositoryByExternalID(ctx, repositoryExternalID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByGithubID", reflect.TypeOf((*MockRepository)(nil).GetRepositoryByGithubID), ctx, externalID, enabled) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetRepositoryByExternalID", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetRepositoryByExternalID), ctx, repositoryExternalID) } -// GetRepositoriesByCLAGroup mocks base method -func (m *MockRepository) GetRepositoriesByCLAGroup(ctx context.Context, claGroup string, enabled bool) ([]*models.GithubRepository, error) { +// GitHubGetRepositoryByGithubID mocks base method. +func (m *MockRepositoryInterface) GitHubGetRepositoryByGithubID(ctx context.Context, externalID string, enabled bool) (*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoriesByCLAGroup", ctx, claGroup, enabled) - ret0, _ := ret[0].([]*models.GithubRepository) + ret := m.ctrl.Call(m, "GitHubGetRepositoryByGithubID", ctx, externalID, enabled) + ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepositoriesByCLAGroup indicates an expected call of GetRepositoriesByCLAGroup -func (mr *MockRepositoryMockRecorder) GetRepositoriesByCLAGroup(ctx, claGroup, enabled interface{}) *gomock.Call { +// GitHubGetRepositoryByGithubID indicates an expected call of GitHubGetRepositoryByGithubID. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetRepositoryByGithubID(ctx, externalID, enabled interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoriesByCLAGroup", reflect.TypeOf((*MockRepository)(nil).GetRepositoriesByCLAGroup), ctx, claGroup, enabled) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetRepositoryByGithubID", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetRepositoryByGithubID), ctx, externalID, enabled) } -// GetRepositoriesByOrganizationName mocks base method -func (m *MockRepository) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { +// GitHubGetRepositoryByName mocks base method. +func (m *MockRepositoryInterface) GitHubGetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoriesByOrganizationName", ctx, gitHubOrgName) - ret0, _ := ret[0].([]*models.GithubRepository) + ret := m.ctrl.Call(m, "GitHubGetRepositoryByName", ctx, repositoryName) + ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepositoriesByOrganizationName indicates an expected call of GetRepositoriesByOrganizationName -func (mr *MockRepositoryMockRecorder) GetRepositoriesByOrganizationName(ctx, gitHubOrgName interface{}) *gomock.Call { +// GitHubGetRepositoryByName indicates an expected call of GitHubGetRepositoryByName. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubGetRepositoryByName(ctx, repositoryName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoriesByOrganizationName", reflect.TypeOf((*MockRepository)(nil).GetRepositoriesByOrganizationName), ctx, gitHubOrgName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubGetRepositoryByName", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubGetRepositoryByName), ctx, repositoryName) } -// GetCLAGroupRepositoriesGroupByOrgs mocks base method -func (m *MockRepository) GetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepositoriesGroupByOrgs, error) { +// GitHubListProjectRepositories mocks base method. +func (m *MockRepositoryInterface) GitHubListProjectRepositories(ctx context.Context, projectSFID string, enabled *bool) (*models.GithubListRepositories, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCLAGroupRepositoriesGroupByOrgs", ctx, projectID, enabled) - ret0, _ := ret[0].([]*models.GithubRepositoriesGroupByOrgs) + ret := m.ctrl.Call(m, "GitHubListProjectRepositories", ctx, projectSFID, enabled) + ret0, _ := ret[0].(*models.GithubListRepositories) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetCLAGroupRepositoriesGroupByOrgs indicates an expected call of GetCLAGroupRepositoriesGroupByOrgs -func (mr *MockRepositoryMockRecorder) GetCLAGroupRepositoriesGroupByOrgs(ctx, projectID, enabled interface{}) *gomock.Call { +// GitHubListProjectRepositories indicates an expected call of GitHubListProjectRepositories. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubListProjectRepositories(ctx, projectSFID, enabled interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCLAGroupRepositoriesGroupByOrgs", reflect.TypeOf((*MockRepository)(nil).GetCLAGroupRepositoriesGroupByOrgs), ctx, projectID, enabled) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubListProjectRepositories", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubListProjectRepositories), ctx, projectSFID, enabled) } -// ListProjectRepositories mocks base method -func (m *MockRepository) ListProjectRepositories(ctx context.Context, externalProjectID, projectSFID string, enabled bool) (*models.ListGithubRepositories, error) { +// GitHubSetRemoteDeletedRepository mocks base method. +func (m *MockRepositoryInterface) GitHubSetRemoteDeletedRepository(ctx context.Context, repositoryID string, isDeleted, wasCLAEnforced bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListProjectRepositories", ctx, externalProjectID, projectSFID, enabled) - ret0, _ := ret[0].(*models.ListGithubRepositories) + ret := m.ctrl.Call(m, "GitHubSetRemoteDeletedRepository", ctx, repositoryID, isDeleted, wasCLAEnforced) + ret0, _ := ret[0].(error) + return ret0 +} + +// GitHubSetRemoteDeletedRepository indicates an expected call of GitHubSetRemoteDeletedRepository. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubSetRemoteDeletedRepository(ctx, repositoryID, isDeleted, wasCLAEnforced interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubSetRemoteDeletedRepository", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubSetRemoteDeletedRepository), ctx, repositoryID, isDeleted, wasCLAEnforced) +} + +// GitHubUpdateClaGroupID mocks base method. +func (m *MockRepositoryInterface) GitHubUpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GitHubUpdateClaGroupID", ctx, repositoryID, claGroupID) + ret0, _ := ret[0].(error) + return ret0 +} + +// GitHubUpdateClaGroupID indicates an expected call of GitHubUpdateClaGroupID. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubUpdateClaGroupID(ctx, repositoryID, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubUpdateClaGroupID", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubUpdateClaGroupID), ctx, repositoryID, claGroupID) +} + +// GitHubUpdateRepository mocks base method. +func (m *MockRepositoryInterface) GitHubUpdateRepository(ctx context.Context, repositoryID, projectSFID, parentProjectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GitHubUpdateRepository", ctx, repositoryID, projectSFID, parentProjectSFID, input) + ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListProjectRepositories indicates an expected call of ListProjectRepositories -func (mr *MockRepositoryMockRecorder) ListProjectRepositories(ctx, externalProjectID, projectSFID, enabled interface{}) *gomock.Call { +// GitHubUpdateRepository indicates an expected call of GitHubUpdateRepository. +func (mr *MockRepositoryInterfaceMockRecorder) GitHubUpdateRepository(ctx, repositoryID, projectSFID, parentProjectSFID, input interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProjectRepositories", reflect.TypeOf((*MockRepository)(nil).ListProjectRepositories), ctx, externalProjectID, projectSFID, enabled) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitHubUpdateRepository", reflect.TypeOf((*MockRepositoryInterface)(nil).GitHubUpdateRepository), ctx, repositoryID, projectSFID, parentProjectSFID, input) } diff --git a/cla-backend-go/repositories/mock/mock_service.go b/cla-backend-go/repositories/mock/mock_service.go index fbca307a3..49642a474 100644 --- a/cla-backend-go/repositories/mock/mock_service.go +++ b/cla-backend-go/repositories/mock/mock_service.go @@ -12,34 +12,34 @@ import ( context "context" reflect "reflect" - models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" gomock "github.com/golang/mock/gomock" ) -// MockService is a mock of Service interface +// MockService is a mock of Service interface. type MockService struct { ctrl *gomock.Controller recorder *MockServiceMockRecorder } -// MockServiceMockRecorder is the mock recorder for MockService +// MockServiceMockRecorder is the mock recorder for MockService. type MockServiceMockRecorder struct { mock *MockService } -// NewMockService creates a new mock instance +// 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 +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockService) EXPECT() *MockServiceMockRecorder { return m.recorder } -// AddGithubRepository mocks base method +// AddGithubRepository mocks base method. func (m *MockService) AddGithubRepository(ctx context.Context, externalProjectID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddGithubRepository", ctx, externalProjectID, input) @@ -48,87 +48,100 @@ func (m *MockService) AddGithubRepository(ctx context.Context, externalProjectID return ret0, ret1 } -// AddGithubRepository indicates an expected call of AddGithubRepository +// AddGithubRepository indicates an expected call of AddGithubRepository. func (mr *MockServiceMockRecorder) AddGithubRepository(ctx, externalProjectID, input interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGithubRepository", reflect.TypeOf((*MockService)(nil).AddGithubRepository), ctx, externalProjectID, input) } -// GetRepositoryByName mocks base method -func (m *MockService) GetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) { +// DisableRepositoriesByProjectID mocks base method. +func (m *MockService) DisableRepositoriesByProjectID(ctx context.Context, projectID string) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoryByName", ctx, repositoryName) - ret0, _ := ret[0].(*models.GithubRepository) + ret := m.ctrl.Call(m, "DisableRepositoriesByProjectID", ctx, projectID) + ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } -// EnableRepository mocks base method -func (m *MockService) EnableRepository(ctx context.Context, repositoryID string) error { +// DisableRepositoriesByProjectID indicates an expected call of DisableRepositoriesByProjectID. +func (mr *MockServiceMockRecorder) DisableRepositoriesByProjectID(ctx, projectID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepositoriesByProjectID", reflect.TypeOf((*MockService)(nil).DisableRepositoriesByProjectID), ctx, projectID) +} + +// DisableRepository mocks base method. +func (m *MockService) DisableRepository(ctx context.Context, repositoryID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnableRepository", ctx, repositoryID) + ret := m.ctrl.Call(m, "DisableRepository", ctx, repositoryID) ret0, _ := ret[0].(error) return ret0 } -// EnableRepositoryWithCLAGroupID mocks base method -func (m *MockService) EnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { +// DisableRepository indicates an expected call of DisableRepository. +func (mr *MockServiceMockRecorder) DisableRepository(ctx, repositoryID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepository", reflect.TypeOf((*MockService)(nil).DisableRepository), ctx, repositoryID) +} + +// EnableRepository mocks base method. +func (m *MockService) EnableRepository(ctx context.Context, repositoryID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnableRepositoryWithCLAGroupID", ctx, repositoryID, claGroupID) + ret := m.ctrl.Call(m, "EnableRepository", ctx, repositoryID) ret0, _ := ret[0].(error) return ret0 } -// EnableRepository indicates an expected call of EnableRepository +// EnableRepository indicates an expected call of EnableRepository. func (mr *MockServiceMockRecorder) EnableRepository(ctx, repositoryID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableRepository", reflect.TypeOf((*MockService)(nil).EnableRepository), ctx, repositoryID) } -// DisableRepository mocks base method -func (m *MockService) DisableRepository(ctx context.Context, repositoryID string) error { +// EnableRepositoryWithCLAGroupID mocks base method. +func (m *MockService) EnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisableRepository", ctx, repositoryID) + ret := m.ctrl.Call(m, "EnableRepositoryWithCLAGroupID", ctx, repositoryID, claGroupID) ret0, _ := ret[0].(error) return ret0 } -// DisableRepository indicates an expected call of DisableRepository -func (mr *MockServiceMockRecorder) DisableRepository(ctx, repositoryID interface{}) *gomock.Call { +// EnableRepositoryWithCLAGroupID indicates an expected call of EnableRepositoryWithCLAGroupID. +func (mr *MockServiceMockRecorder) EnableRepositoryWithCLAGroupID(ctx, repositoryID, claGroupID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepository", reflect.TypeOf((*MockService)(nil).DisableRepository), ctx, repositoryID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableRepositoryWithCLAGroupID", reflect.TypeOf((*MockService)(nil).EnableRepositoryWithCLAGroupID), ctx, repositoryID, claGroupID) } -// UpdateClaGroupID mocks base method -func (m *MockService) UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { +// GetRepositoriesByCLAGroup mocks base method. +func (m *MockService) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string) ([]*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateClaGroupID", ctx, repositoryID, claGroupID) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "GetRepositoriesByCLAGroup", ctx, claGroupID) + ret0, _ := ret[0].([]*models.GithubRepository) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// UpdateClaGroupID indicates an expected call of UpdateClaGroupID -func (mr *MockServiceMockRecorder) UpdateClaGroupID(ctx, repositoryID, claGroupID interface{}) *gomock.Call { +// GetRepositoriesByCLAGroup indicates an expected call of GetRepositoriesByCLAGroup. +func (mr *MockServiceMockRecorder) GetRepositoriesByCLAGroup(ctx, claGroupID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClaGroupID", reflect.TypeOf((*MockService)(nil).UpdateClaGroupID), ctx, repositoryID, claGroupID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoriesByCLAGroup", reflect.TypeOf((*MockService)(nil).GetRepositoriesByCLAGroup), ctx, claGroupID) } -// ListProjectRepositories mocks base method -func (m *MockService) ListProjectRepositories(ctx context.Context, externalProjectID string, enabled *bool) (*models.ListGithubRepositories, error) { +// GetRepositoriesByOrganizationName mocks base method. +func (m *MockService) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListProjectRepositories", ctx, externalProjectID, enabled) - ret0, _ := ret[0].(*models.ListGithubRepositories) + ret := m.ctrl.Call(m, "GetRepositoriesByOrganizationName", ctx, gitHubOrgName) + ret0, _ := ret[0].([]*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListProjectRepositories indicates an expected call of ListProjectRepositories -func (mr *MockServiceMockRecorder) ListProjectRepositories(ctx, externalProjectID interface{}, enabled *bool) *gomock.Call { +// GetRepositoriesByOrganizationName indicates an expected call of GetRepositoriesByOrganizationName. +func (mr *MockServiceMockRecorder) GetRepositoriesByOrganizationName(ctx, gitHubOrgName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProjectRepositories", reflect.TypeOf((*MockService)(nil).ListProjectRepositories), ctx, externalProjectID, enabled) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoriesByOrganizationName", reflect.TypeOf((*MockService)(nil).GetRepositoriesByOrganizationName), ctx, gitHubOrgName) } -// GetRepository mocks base method +// GetRepository mocks base method. func (m *MockService) GetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRepository", ctx, repositoryID) @@ -137,121 +150,150 @@ func (m *MockService) GetRepository(ctx context.Context, repositoryID string) (* return ret0, ret1 } -// GetRepository indicates an expected call of GetRepository +// GetRepository indicates an expected call of GetRepository. func (mr *MockServiceMockRecorder) GetRepository(ctx, repositoryID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepository", reflect.TypeOf((*MockService)(nil).GetRepository), ctx, repositoryID) } -// GetRepository mocks base method -func (m *MockService) GetRepositoryByProjectSFID(ctx context.Context, projectSFID string, enabled *bool) (*models.ListGithubRepositories, error) { +// GetRepositoryByExternalID mocks base method. +func (m *MockService) GetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoryByProjectSFID", ctx, projectSFID, enabled) - ret0, _ := ret[0].(*models.ListGithubRepositories) + ret := m.ctrl.Call(m, "GetRepositoryByExternalID", ctx, repositoryExternalID) + ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepository indicates an expected call of GetRepository -func (mr *MockServiceMockRecorder) GetRepositoryByProjectSFID(ctx, projectSFID interface{}, enabled *bool) *gomock.Call { +// GetRepositoryByExternalID indicates an expected call of GetRepositoryByExternalID. +func (mr *MockServiceMockRecorder) GetRepositoryByExternalID(ctx, repositoryExternalID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByProjectSFID", reflect.TypeOf((*MockService)(nil).GetRepositoryByProjectSFID), ctx, projectSFID, enabled) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByExternalID", reflect.TypeOf((*MockService)(nil).GetRepositoryByExternalID), ctx, repositoryExternalID) } -// DisableRepositoriesByProjectID mocks base method -func (m *MockService) DisableRepositoriesByProjectID(ctx context.Context, projectID string) (int, error) { +// GetRepositoryByName mocks base method. +func (m *MockService) GetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisableRepositoriesByProjectID", ctx, projectID) - ret0, _ := ret[0].(int) + ret := m.ctrl.Call(m, "GetRepositoryByName", ctx, repositoryName) + ret0, _ := ret[0].(*models.GithubRepository) ret1, _ := ret[1].(error) return ret0, ret1 } -// DisableRepositoriesByProjectID indicates an expected call of DisableRepositoriesByProjectID -func (mr *MockServiceMockRecorder) DisableRepositoriesByProjectID(ctx, projectID interface{}) *gomock.Call { +// GetRepositoryByName indicates an expected call of GetRepositoryByName. +func (mr *MockServiceMockRecorder) GetRepositoryByName(ctx, repositoryName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableRepositoriesByProjectID", reflect.TypeOf((*MockService)(nil).DisableRepositoriesByProjectID), ctx, projectID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByName", reflect.TypeOf((*MockService)(nil).GetRepositoryByName), ctx, repositoryName) } -// GetRepositoriesByCLAGroup mocks base method -func (m *MockService) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string) ([]*models.GithubRepository, error) { +// GetRepositoryByProjectSFID mocks base method. +func (m *MockService) GetRepositoryByProjectSFID(ctx context.Context, projectSFID string, enabled *bool) (*models.GithubListRepositories, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoriesByCLAGroup", ctx, claGroupID) - ret0, _ := ret[0].([]*models.GithubRepository) + ret := m.ctrl.Call(m, "GetRepositoryByProjectSFID", ctx, projectSFID, enabled) + ret0, _ := ret[0].(*models.GithubListRepositories) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepositoriesByCLAGroup indicates an expected call of GetRepositoriesByCLAGroup -func (mr *MockServiceMockRecorder) GetRepositoriesByCLAGroup(ctx, claGroupID interface{}) *gomock.Call { +// GetRepositoryByProjectSFID indicates an expected call of GetRepositoryByProjectSFID. +func (mr *MockServiceMockRecorder) GetRepositoryByProjectSFID(ctx, projectSFID, enabled interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoriesByCLAGroup", reflect.TypeOf((*MockService)(nil).GetRepositoriesByCLAGroup), ctx, claGroupID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByProjectSFID", reflect.TypeOf((*MockService)(nil).GetRepositoryByProjectSFID), ctx, projectSFID, enabled) } -// GetRepositoriesByOrganizationName mocks base method -func (m *MockService) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { +// ListProjectRepositories mocks base method. +func (m *MockService) ListProjectRepositories(ctx context.Context, externalProjectID string, enabled *bool) (*models.GithubListRepositories, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRepositoriesByOrganizationName", ctx, gitHubOrgName) - ret0, _ := ret[0].([]*models.GithubRepository) + ret := m.ctrl.Call(m, "ListProjectRepositories", ctx, externalProjectID, enabled) + ret0, _ := ret[0].(*models.GithubListRepositories) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRepositoriesByOrganizationName indicates an expected call of GetRepositoriesByOrganizationName -func (mr *MockServiceMockRecorder) GetRepositoriesByOrganizationName(ctx, gitHubOrgName interface{}) *gomock.Call { +// ListProjectRepositories indicates an expected call of ListProjectRepositories. +func (mr *MockServiceMockRecorder) ListProjectRepositories(ctx, externalProjectID, enabled interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoriesByOrganizationName", reflect.TypeOf((*MockService)(nil).GetRepositoriesByOrganizationName), ctx, gitHubOrgName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProjectRepositories", reflect.TypeOf((*MockService)(nil).ListProjectRepositories), ctx, externalProjectID, enabled) +} + +// UpdateClaGroupID mocks base method. +func (m *MockService) UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateClaGroupID", ctx, repositoryID, claGroupID) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateClaGroupID indicates an expected call of UpdateClaGroupID. +func (mr *MockServiceMockRecorder) UpdateClaGroupID(ctx, repositoryID, claGroupID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClaGroupID", reflect.TypeOf((*MockService)(nil).UpdateClaGroupID), ctx, repositoryID, claGroupID) } -// MockGithubOrgRepo is a mock of GithubOrgRepo interface +// MockGithubOrgRepo is a mock of GithubOrgRepo interface. type MockGithubOrgRepo struct { ctrl *gomock.Controller recorder *MockGithubOrgRepoMockRecorder } -// MockGithubOrgRepoMockRecorder is the mock recorder for MockGithubOrgRepo +// MockGithubOrgRepoMockRecorder is the mock recorder for MockGithubOrgRepo. type MockGithubOrgRepoMockRecorder struct { mock *MockGithubOrgRepo } -// NewMockGithubOrgRepo creates a new mock instance +// NewMockGithubOrgRepo creates a new mock instance. func NewMockGithubOrgRepo(ctrl *gomock.Controller) *MockGithubOrgRepo { mock := &MockGithubOrgRepo{ctrl: ctrl} mock.recorder = &MockGithubOrgRepoMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockGithubOrgRepo) EXPECT() *MockGithubOrgRepoMockRecorder { return m.recorder } -// GetGithubOrganizationByName mocks base method -func (m *MockGithubOrgRepo) GetGithubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) { +// GetGitHubOrganization mocks base method. +func (m *MockGithubOrgRepo) GetGitHubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGitHubOrganization", ctx, githubOrganizationName) + ret0, _ := ret[0].(*models.GithubOrganization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGitHubOrganization indicates an expected call of GetGitHubOrganization. +func (mr *MockGithubOrgRepoMockRecorder) GetGitHubOrganization(ctx, githubOrganizationName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganization", reflect.TypeOf((*MockGithubOrgRepo)(nil).GetGitHubOrganization), ctx, githubOrganizationName) +} + +// GetGitHubOrganizationByName mocks base method. +func (m *MockGithubOrgRepo) GetGitHubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGithubOrganizationByName", ctx, githubOrganizationName) + ret := m.ctrl.Call(m, "GetGitHubOrganizationByName", ctx, githubOrganizationName) ret0, _ := ret[0].(*models.GithubOrganizations) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetGithubOrganizationByName indicates an expected call of GetGithubOrganizationByName -func (mr *MockGithubOrgRepoMockRecorder) GetGithubOrganizationByName(ctx, githubOrganizationName interface{}) *gomock.Call { +// GetGitHubOrganizationByName indicates an expected call of GetGitHubOrganizationByName. +func (mr *MockGithubOrgRepoMockRecorder) GetGitHubOrganizationByName(ctx, githubOrganizationName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganizationByName", reflect.TypeOf((*MockGithubOrgRepo)(nil).GetGithubOrganizationByName), ctx, githubOrganizationName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganizationByName", reflect.TypeOf((*MockGithubOrgRepo)(nil).GetGitHubOrganizationByName), ctx, githubOrganizationName) } -// GetGithubOrganization mocks base method -func (m *MockGithubOrgRepo) GetGithubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) { +// GetGitHubOrganizations mocks base method. +func (m *MockGithubOrgRepo) GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGithubOrganization", ctx, githubOrganizationName) - ret0, _ := ret[0].(*models.GithubOrganization) + ret := m.ctrl.Call(m, "GetGitHubOrganizations", ctx, projectSFID) + ret0, _ := ret[0].(*models.GithubOrganizations) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetGithubOrganization indicates an expected call of GetGithubOrganization -func (mr *MockGithubOrgRepoMockRecorder) GetGithubOrganization(ctx, githubOrganizationName interface{}) *gomock.Call { +// GetGitHubOrganizations indicates an expected call of GetGitHubOrganizations. +func (mr *MockGithubOrgRepoMockRecorder) GetGitHubOrganizations(ctx, projectSFID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGithubOrganization", reflect.TypeOf((*MockGithubOrgRepo)(nil).GetGithubOrganization), ctx, githubOrganizationName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitHubOrganizations", reflect.TypeOf((*MockGithubOrgRepo)(nil).GetGitHubOrganizations), ctx, projectSFID) } diff --git a/cla-backend-go/repositories/models.go b/cla-backend-go/repositories/models.go index da8ec82ee..73862c6bb 100644 --- a/cla-backend-go/repositories/models.go +++ b/cla-backend-go/repositories/models.go @@ -3,17 +3,23 @@ package repositories -import "github.com/communitybridge/easycla/cla-backend-go/gen/models" +import ( + "strconv" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) // RepositoryDBModel represent repositories table type RepositoryDBModel struct { DateCreated string `dynamodbav:"date_created" json:"date_created,omitempty"` DateModified string `dynamodbav:"date_modified" json:"date_modified,omitempty"` - RepositoryExternalID string `dynamodbav:"repository_external_id" json:"repository_external_id,omitempty"` + RepositoryExternalID string `dynamodbav:"repository_external_id" json:"repository_external_id,omitempty"` // Integer value from GitHub RepositoryID string `dynamodbav:"repository_id" json:"repository_id,omitempty"` RepositoryName string `dynamodbav:"repository_name" json:"repository_name,omitempty"` + RepositoryFullPath string `dynamodbav:"repository_full_path" json:"repository_full_path,omitempty"` RepositoryOrganizationName string `dynamodbav:"repository_organization_name" json:"repository_organization_name,omitempty"` - RepositoryProjectID string `dynamodbav:"repository_project_id" json:"repository_project_id,omitempty"` + RepositoryCLAGroupID string `dynamodbav:"repository_project_id" json:"repository_project_id,omitempty"` RepositorySfdcID string `dynamodbav:"repository_sfdc_id" json:"repository_sfdc_id,omitempty"` RepositoryType string `dynamodbav:"repository_type" json:"repository_type,omitempty"` RepositoryURL string `dynamodbav:"repository_url" json:"repository_url,omitempty"` @@ -21,32 +27,45 @@ type RepositoryDBModel struct { Enabled bool `dynamodbav:"enabled" json:"enabled"` Note string `dynamodbav:"note" json:"note,omitempty"` Version string `dynamodbav:"version" json:"version,omitempty"` + IsRemoteDeleted bool `dynamodbav:"is_remote_deleted" json:"is_transfered,omitempty"` + WasCLAEnforced bool `dynamodbav:"was_cla_enforced" json:"was_cla_enforced,omitempty"` } func convertModels(dbModels []*RepositoryDBModel) []*models.GithubRepository { var responseModels []*models.GithubRepository for _, dbModel := range dbModels { - responseModels = append(responseModels, dbModel.toModel()) - + // Apply condition, don't return repositories which are remotely deleted. + if !dbModel.IsRemoteDeleted { + responseModels = append(responseModels, dbModel.ToGitHubModel()) + } } return responseModels } -func (gr *RepositoryDBModel) toModel() *models.GithubRepository { +// ToGitHubModel returns the database model to a GitHub repository model suitable for marshalling to the client +func (gr *RepositoryDBModel) ToGitHubModel() *models.GithubRepository { + gitLabExternalID, err := strconv.ParseInt(gr.RepositoryExternalID, 10, 64) + if err != nil { + log.WithError(err).Warnf("unable to convert repository external ID to an int64 value: %s", gr.RepositoryExternalID) + return nil + } + return &models.GithubRepository{ DateCreated: gr.DateCreated, DateModified: gr.DateModified, - RepositoryExternalID: gr.RepositoryExternalID, + RepositoryExternalID: gitLabExternalID, RepositoryID: gr.RepositoryID, RepositoryName: gr.RepositoryName, RepositoryOrganizationName: gr.RepositoryOrganizationName, - RepositoryProjectID: gr.RepositoryProjectID, + RepositoryProjectSfid: gr.ProjectSFID, RepositorySfdcID: gr.RepositorySfdcID, RepositoryType: gr.RepositoryType, RepositoryURL: gr.RepositoryURL, - ProjectSFID: gr.ProjectSFID, + RepositoryClaGroupID: gr.RepositoryCLAGroupID, Enabled: gr.Enabled, Note: gr.Note, Version: gr.Version, + WasClaEnforced: gr.WasCLAEnforced, + IsRemoteDeleted: gr.IsRemoteDeleted, } } diff --git a/cla-backend-go/repositories/repository.go b/cla-backend-go/repositories/repository.go index 3f3d5c9fb..c97f63aa7 100644 --- a/cla-backend-go/repositories/repository.go +++ b/cla-backend-go/repositories/repository.go @@ -7,6 +7,8 @@ import ( "context" "errors" "fmt" + "strconv" + "strings" "github.com/sirupsen/logrus" @@ -22,70 +24,62 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" - "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" ) // index const ( - repositoryEnabledColumn = "enabled" - - // RepositoryEnabled flag - RepositoryEnabled = "enabled" - - // RepositoryDisabled flag - RepositoryDisabled = "disabled" - - ProjectRepositoryIndex = "project-repository-index" - SFDCRepositoryIndex = "sfdc-repository-index" - ExternalRepositoryIndex = "external-repository-index" - ProjectSFIDRepositoryOrganizationNameIndex = "project-sfid-repository-organization-name-index" - RepositoryOrganizationNameIndex = "repository-organization-name-index" - RepositoryNameIndex = "repository-name-index" + repositoryEnabledColumn = "enabled" + repositoryRemoteDeletedColumn = "is_remote_deleted" + repositoryWasCLAEnforcedColumn = "was_cla_enforced" ) -// errors -var ( - ErrGithubRepositoryNotFound = errors.New(utils.GithubRepoNotFound) -) - -// Repository defines functions of Repositories -type Repository interface { - AddGithubRepository(ctx context.Context, externalProjectID string, projectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) - UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error - EnableRepository(ctx context.Context, repositoryID string) error - EnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error - DisableRepository(ctx context.Context, repositoryID string) error - DisableRepositoriesByProjectID(ctx context.Context, projectID string) error - DisableRepositoriesOfGithubOrganization(ctx context.Context, externalProjectID, githubOrgName string) error - GetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) - GetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) - GetRepositoryByGithubID(ctx context.Context, externalID string, enabled bool) (*models.GithubRepository, error) - GetRepositoriesByCLAGroup(ctx context.Context, claGroup string, enabled bool) ([]*models.GithubRepository, error) - GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) - GetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepositoriesGroupByOrgs, error) - ListProjectRepositories(ctx context.Context, externalProjectID string, projectSFID string, enabled *bool) (*models.ListGithubRepositories, error) +// ErrRepositoryDoesNotExist ... +var ErrRepositoryDoesNotExist = errors.New("repository does not exist") + +// RepositoryInterface contains functions of the repositories service +type RepositoryInterface interface { + GitHubAddRepository(ctx context.Context, externalProjectID string, projectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) + GitHubUpdateRepository(ctx context.Context, repositoryID, projectSFID, parentProjectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) + GitHubUpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error + GitHubEnableRepository(ctx context.Context, repositoryID string) error + GitHubEnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error + GitHubDisableRepository(ctx context.Context, repositoryID string) error + GitHubDisableRepositoriesByProjectID(ctx context.Context, projectID string) error + GitHubDisableRepositoriesOfOrganization(ctx context.Context, externalProjectID, githubOrgName string) error + GitHubDisableRepositoriesOfOrganizationParent(ctx context.Context, parentProjectSFID, githubOrgName string) error + GitHubGetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) + GitHubGetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) + GitHubGetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*models.GithubRepository, error) + GitHubGetRepositoryByGithubID(ctx context.Context, externalID string, enabled bool) (*models.GithubRepository, error) + GitHubGetRepositoriesByCLAGroup(ctx context.Context, claGroup string, enabled bool) ([]*models.GithubRepository, error) + GitHubGetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) + GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepositoriesGroupByOrgs, error) + GitHubListProjectRepositories(ctx context.Context, projectSFID string, enabled *bool) (*models.GithubListRepositories, error) + GitHubSetRemoteDeletedRepository(ctx context.Context, repositoryID string, isDeleted bool, wasCLAEnforced bool) error } // NewRepository create new Repository -func NewRepository(awsSession *session.Session, stage string) Repository { - return &repo{ +func NewRepository(awsSession *session.Session, stage string) *Repository { + return &Repository{ stage: stage, dynamoDBClient: dynamodb.New(awsSession), repositoryTableName: fmt.Sprintf("cla-%s-repositories", stage), } } -type repo struct { +// Repository structure +type Repository struct { stage string dynamoDBClient *dynamodb.DynamoDB repositoryTableName string } -// AddGithubRepository adds the specified repository -func (r repo) AddGithubRepository(ctx context.Context, externalProjectID string, projectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { +// GitHubAddRepository adds the specified repository +func (r *Repository) GitHubAddRepository(ctx context.Context, externalProjectID string, projectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "AddGitHubRepository", + "functionName": "v1.repositories.repository.AddGitHubRepository", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "externalProjectID": externalProjectID, "projectSFID": projectSFID, @@ -96,14 +90,32 @@ func (r repo) AddGithubRepository(ctx context.Context, externalProjectID string, } // Check first to see if the repository already exists - _, err := r.GetRepositoryByGithubID(ctx, utils.StringValue(input.RepositoryExternalID), true) + repo, err := r.GitHubGetRepositoryByExternalID(ctx, utils.StringValue(input.RepositoryExternalID)) if err != nil { // Expecting Not found - no issue if not found - all other error we throw - if err != ErrGithubRepositoryNotFound { + if _, ok := err.(*utils.GitHubRepositoryNotFound); !ok { return nil, err } } else { - return nil, errors.New("github repository already exist") + if repo.Enabled { + return nil, errors.New("github repository already exist") + } + // Here repository already exists. We update the same repository with latest document in order to avoid duplicate entries. + var enabled = true + var repository *models.GithubRepository + repository, err = r.GitHubUpdateRepository(ctx, repo.RepositoryID, projectSFID, externalProjectID, &models.GithubRepositoryInput{ + RepositoryName: input.RepositoryName, + RepositoryOrganizationName: input.RepositoryOrganizationName, + RepositoryProjectID: input.RepositoryProjectID, + Enabled: &enabled, + RepositoryType: input.RepositoryType, + RepositoryURL: input.RepositoryURL, + Note: "Disabled repository enabled and updated", + }) + if err != nil { + return nil, err + } + return repository, nil } _, currentTime := utils.CurrentTime() @@ -119,7 +131,7 @@ func (r repo) AddGithubRepository(ctx context.Context, externalProjectID string, RepositoryID: repoID.String(), RepositoryName: utils.StringValue(input.RepositoryName), RepositoryOrganizationName: utils.StringValue(input.RepositoryOrganizationName), - RepositoryProjectID: utils.StringValue(input.RepositoryProjectID), + RepositoryCLAGroupID: utils.StringValue(input.RepositoryProjectID), RepositorySfdcID: externalProjectID, RepositoryType: utils.StringValue(input.RepositoryType), RepositoryURL: utils.StringValue(input.RepositoryURL), @@ -144,30 +156,183 @@ func (r repo) AddGithubRepository(ctx context.Context, externalProjectID string, return nil, err } - return repository.toModel(), nil + return repository.ToGitHubModel(), nil } -// UpdateClaGroupID updates the claGroupID of the repository -func (r *repo) UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { +// GitHubUpdateRepository updates the repository record for given ID +func (r *Repository) GitHubUpdateRepository(ctx context.Context, repositoryID, projectSFID, parentProjectSFID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { + + externalID := utils.StringValue(input.RepositoryExternalID) + repositoryName := utils.StringValue(input.RepositoryName) + repositoryOrganizationName := utils.StringValue(input.RepositoryOrganizationName) + repositoryType := utils.StringValue(input.RepositoryType) + repositoryURL := utils.StringValue(input.RepositoryURL) + note := input.Note + + f := logrus.Fields{ + "functionName": "v1.repositories.repository.UpdateGitHubRepository", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "repositoryID": repositoryID, + "externalProjectID": externalID, + "repositoryName": repositoryName, + "repositoryOrganizationName": repositoryOrganizationName, + "repositoryType": repositoryType, + "repositoryURL": repositoryURL, + "projectSFDID": projectSFID, + "parentProjectSFID": parentProjectSFID, + } + + log.WithFields(f).Debugf("updating CombinedRepository : %s... ", repositoryID) + + repoModel, repoErr := r.GitHubGetRepository(ctx, repositoryID) + if repoErr != nil { + log.WithFields(f).Warnf("update error locating the repository ID : %s , error: %+v ", repositoryID, repoErr) + return nil, repoErr + } + + if repoModel == nil { + log.WithFields(f).Warnf("CombinedRepository does not exist for *Repository: %s ", repositoryID) + return nil, ErrRepositoryDoesNotExist + } + + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + updateExpression := "SET " + + // Convert the numeric value to a string for the DB + externalIDStr := strconv.FormatInt(repoModel.RepositoryExternalID, 10) + if externalID != "" && externalIDStr != externalID { + log.WithFields(f).Debugf("adding externalID : %s ", externalID) + expressionAttributeNames["#E"] = aws.String("repository_external_id") + expressionAttributeValues[":e"] = &dynamodb.AttributeValue{S: aws.String(externalID)} + updateExpression = updateExpression + " #E = :e, " + } + + if repositoryName != "" && repoModel.RepositoryName != repositoryName { + log.WithFields(f).Debugf("adding repositoryName : %s ", repositoryName) + expressionAttributeNames["#N"] = aws.String("repository_name") + expressionAttributeValues[":n"] = &dynamodb.AttributeValue{S: aws.String(repositoryName)} + updateExpression = updateExpression + " #N = :n, " + } + + if repositoryOrganizationName != "" && repoModel.RepositoryOrganizationName != repositoryOrganizationName { + log.WithFields(f).Debugf("adding repositoryOrganizationName : %s ", repositoryOrganizationName) + expressionAttributeNames["#O"] = aws.String("repository_organization_name") + expressionAttributeValues[":o"] = &dynamodb.AttributeValue{S: aws.String(repositoryOrganizationName)} + updateExpression = updateExpression + " #O = :o, " + } + + if repositoryType != "" && repoModel.RepositoryType != repositoryType { + log.WithFields(f).Debugf("adding repositoryType : %s ", repositoryType) + expressionAttributeNames["#T"] = aws.String("repository_type") + expressionAttributeValues[":t"] = &dynamodb.AttributeValue{S: aws.String(repositoryType)} + updateExpression = updateExpression + " #T = :t, " + } + + if repositoryURL != "" && repoModel.RepositoryURL != repositoryURL { + log.WithFields(f).Debugf("adding repositoryURL : %s ", repositoryURL) + expressionAttributeNames["#U"] = aws.String("repository_url") + expressionAttributeValues[":u"] = &dynamodb.AttributeValue{S: aws.String(repositoryURL)} + updateExpression = updateExpression + " #U = :u, " + } + + if note != "" { + log.WithFields(f).Debugf("adding note: %s ", note) + noteValue := note + if !strings.HasSuffix(noteValue, ".") { + noteValue = fmt.Sprintf("%s.", noteValue) + } + // If we have a previous value - just concat the value to the end + if repoModel.Note != "" { + if strings.HasSuffix(strings.TrimSpace(repoModel.Note), ".") { + noteValue = fmt.Sprintf("%s %s", repoModel.Note, noteValue) + } else { + noteValue = fmt.Sprintf("%s. %s", repoModel.Note, noteValue) + } + } + expressionAttributeNames["#NO"] = aws.String("note") + expressionAttributeValues[":no"] = &dynamodb.AttributeValue{S: aws.String(noteValue)} + updateExpression = updateExpression + " #NO = :no, " + } + + if input.Enabled != nil && repoModel.Enabled != *input.Enabled { + log.WithFields(f).Debugf("adding enabled flag: %+v", *input.Enabled) + expressionAttributeNames["#EN"] = aws.String("enabled") + expressionAttributeValues[":en"] = &dynamodb.AttributeValue{BOOL: input.Enabled} + updateExpression = updateExpression + " #EN = :en, " + } + + if input.RepositoryProjectID != nil && repoModel.RepositoryClaGroupID != *input.RepositoryProjectID { + log.WithFields(f).Debugf("adding repositoryProjectID: %+v", *input.RepositoryProjectID) + expressionAttributeNames["#RP"] = aws.String("repository_project_id") + expressionAttributeValues[":rp"] = &dynamodb.AttributeValue{S: input.RepositoryProjectID} + updateExpression = updateExpression + " #RP = :rp, " + } + + if projectSFID != "" && repoModel.RepositoryProjectSfid != projectSFID { + log.WithFields(f).Debugf("adding projectSFID : %s ", projectSFID) + expressionAttributeNames["#P"] = aws.String("project_sfid") + expressionAttributeValues[":p"] = &dynamodb.AttributeValue{S: aws.String(projectSFID)} + updateExpression = updateExpression + " #P = :p, " + } + + if parentProjectSFID != "" { + log.WithFields(f).Debugf("adding parentProjectSFID : %s ", parentProjectSFID) + expressionAttributeNames["#PP"] = aws.String("repository_sfdc_id") + expressionAttributeValues[":pp"] = &dynamodb.AttributeValue{S: aws.String(parentProjectSFID)} + updateExpression = updateExpression + " #PP = :pp, " + } + + _, currentTimeString := utils.CurrentTime() + log.WithFields(f).Debugf("adding date_modified: %s", currentTimeString) + expressionAttributeNames["#M"] = aws.String("date_modified") + expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(currentTimeString)} + updateExpression = updateExpression + " #M = :m " + + // Assemble the query input parameters + updateInput := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "repository_id": { + S: aws.String(repositoryID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(r.repositoryTableName), + } + + _, updateErr := r.dynamoDBClient.UpdateItem(updateInput) + if updateErr != nil { + log.WithFields(f).Warnf("error updatingRepository by repositoryID: %s, error: %v", repositoryID, updateErr) + return nil, updateErr + } + + return r.GitHubGetRepository(ctx, repositoryID) +} + +// GitHubUpdateClaGroupID updates the claGroupID of the repository +func (r *Repository) GitHubUpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { return r.setClaGroupIDGithubRepository(ctx, repositoryID, claGroupID) } -// EnableRepository enables the repository entry -func (r *repo) EnableRepository(ctx context.Context, repositoryID string) error { +// GitHubEnableRepository enables the repository entry +func (r *Repository) GitHubEnableRepository(ctx context.Context, repositoryID string) error { return r.enableGithubRepository(ctx, repositoryID) } -// EnableRepositoryWithCLAGroupID enables the repository entry with the specified CLA Group ID -func (r *repo) EnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { +// GitHubEnableRepositoryWithCLAGroupID enables the repository entry with the specified CLA Group ID +func (r *Repository) GitHubEnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { return r.enableGithubRepositoryWithCLAGroupID(ctx, repositoryID, claGroupID) } -// DisableRepository disables the repository entry (we don't delete) -func (r *repo) DisableRepository(ctx context.Context, repositoryID string) error { +// GitHubDisableRepository disables the repository entry (we don't delete) +func (r *Repository) GitHubDisableRepository(ctx context.Context, repositoryID string) error { return r.disableGithubRepository(ctx, repositoryID) } -func (r *repo) DisableRepositoriesByProjectID(ctx context.Context, projectID string) error { +// GitHubDisableRepositoriesByProjectID disables the repository by the project ID +func (r *Repository) GitHubDisableRepositoriesByProjectID(ctx context.Context, projectID string) error { repoModels, err := r.getProjectRepositories(ctx, projectID, true) if err != nil { return err @@ -175,7 +340,7 @@ func (r *repo) DisableRepositoriesByProjectID(ctx context.Context, projectID str // For each model... for _, repoModel := range repoModels { - disableErr := r.DisableRepository(ctx, repoModel.RepositoryID) + disableErr := r.GitHubDisableRepository(ctx, repoModel.RepositoryID) if disableErr != nil { return disableErr } @@ -184,14 +349,14 @@ func (r *repo) DisableRepositoriesByProjectID(ctx context.Context, projectID str return nil } -// DisableRepositoriesOfGithubOrganization disables the repositories under the GitHub organization -func (r repo) DisableRepositoriesOfGithubOrganization(ctx context.Context, externalProjectID, githubOrgName string) error { +// GitHubDisableRepositoriesOfOrganization disables the repositories under the GitHub organization +func (r *Repository) GitHubDisableRepositoriesOfOrganization(ctx context.Context, projectSFID, githubOrgName string) error { repoModels, err := r.getRepositoriesByGithubOrg(ctx, githubOrgName) if err != nil { return err } for _, repoModel := range repoModels { - if repoModel.RepositoryExternalID == externalProjectID || repoModel.RepositorySfdcID == externalProjectID { + if repoModel.RepositoryProjectSfid == projectSFID { err = r.disableGithubRepository(ctx, repoModel.RepositoryID) if err != nil { return err @@ -201,10 +366,27 @@ func (r repo) DisableRepositoriesOfGithubOrganization(ctx context.Context, exter return nil } -// GetRepository by repository id -func (r *repo) GetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) { +// GitHubDisableRepositoriesOfOrganizationParent disables the repositories under the GitHub organization for the parent project +func (r *Repository) GitHubDisableRepositoriesOfOrganizationParent(ctx context.Context, parentProjectSFID, githubOrgName string) error { + repoModels, err := r.getRepositoriesByGithubOrg(ctx, githubOrgName) + if err != nil { + return err + } + for _, repoModel := range repoModels { + if repoModel.RepositorySfdcID == parentProjectSFID { + err = r.disableGithubRepository(ctx, repoModel.RepositoryID) + if err != nil { + return err + } + } + } + return nil +} + +// GitHubGetRepository by repository id +func (r *Repository) GitHubGetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "GetRepository", + "functionName": "v1.repositories.repository.GitHubGetRepository", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "repositoryID": repositoryID, } @@ -221,8 +403,11 @@ func (r *repo) GetRepository(ctx context.Context, repositoryID string) (*models. return nil, err } if len(result.Item) == 0 { - log.WithFields(f).Warn("repository with ID does not exist") - return nil, ErrGithubRepositoryNotFound + msg := fmt.Sprintf("repository with ID: %s does not exist", repositoryID) + log.WithFields(f).Warn(msg) + return nil, &utils.GitHubRepositoryNotFound{ + Message: msg, + } } var out RepositoryDBModel @@ -232,13 +417,13 @@ func (r *repo) GetRepository(ctx context.Context, repositoryID string) (*models. return nil, err } - return out.toModel(), nil + return out.ToGitHubModel(), nil } -// GetRepositoryByName fetches the repository by repository name -func (r *repo) GetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) { +// GitHubGetRepositoryByName fetches the repository by repository name +func (r *Repository) GitHubGetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "GetRepositoryByName", + "functionName": "v1.repositories.repository.GitHubGetRepositoryByName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "repositoryName": repositoryName, } @@ -269,8 +454,64 @@ func (r *repo) GetRepositoryByName(ctx context.Context, repositoryName string) ( } if len(results.Items) == 0 { - log.WithFields(f).Warn("no repositories found with repository name") - return nil, ErrGithubRepositoryNotFound + log.WithFields(f).Warnf("no repositories found with repository name: %s", repositoryName) + return nil, &utils.GitHubRepositoryNotFound{ + RepositoryName: repositoryName, + } + } + + var repositories []*RepositoryDBModel + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &repositories) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling response") + return nil, err + } + + if len(repositories) > 1 { + log.WithFields(f).Warn("multiple repositories records with the same repository name") + } + + return repositories[0].ToGitHubModel(), nil +} + +// GitHubGetRepositoryByExternalID fetches the repository by repository ID +func (r *Repository) GitHubGetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*models.GithubRepository, error) { + f := logrus.Fields{ + "functionName": "v1.repositories.repository.GitHubGetRepositoryByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "repositoryExternalID": repositoryExternalID, + } + builder := expression.NewBuilder() + condition := expression.Key("repository_external_id").Equal(expression.Value(repositoryExternalID)) + builder = builder.WithKeyCondition(condition) + + expr, err := builder.Build() + if err != nil { + log.WithFields(f).WithError(err).Warn("problem creating builder") + return nil, err + } + + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(r.repositoryTableName), + IndexName: aws.String(RepositoryExternalIDIndex), + } + + results, err := r.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to get repositories by name") + return nil, err + } + + if len(results.Items) == 0 { + log.WithFields(f).Warnf("no repositories found with repository external ID: %s", repositoryExternalID) + return nil, &utils.GitHubRepositoryNotFound{ + RepositoryName: repositoryExternalID, + } } var repositories []*RepositoryDBModel @@ -280,17 +521,17 @@ func (r *repo) GetRepositoryByName(ctx context.Context, repositoryName string) ( return nil, err } - if len(repositories) > 0 { + if len(repositories) > 1 { log.WithFields(f).Warn("multiple repositories records with the same repository name") } - return repositories[0].toModel(), nil + return repositories[0].ToGitHubModel(), nil } -// GetRepositoryByCLAGroup gets the list of repositories based on the CLA Group ID -func (r *repo) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, enabled bool) ([]*models.GithubRepository, error) { +// GitHubGetRepositoriesByCLAGroup gets the list of repositories based on the CLA Group ID +func (r *Repository) GitHubGetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, enabled bool) ([]*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "GetRepositoryByCLAGroup", + "functionName": "v1.repositories.repository.GetRepositoryByCLAGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "enabled": enabled, @@ -312,7 +553,7 @@ func (r *repo) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, ProjectionExpression: expr.Projection(), FilterExpression: expr.Filter(), TableName: aws.String(r.repositoryTableName), - IndexName: aws.String(ProjectRepositoryIndex), + IndexName: aws.String(RepositoryProjectIndex), } results, err := r.dynamoDBClient.Query(queryInput) @@ -322,8 +563,11 @@ func (r *repo) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, } if len(results.Items) == 0 { - log.WithFields(f).Warn("no repositories found matching the search criteria") - return nil, ErrGithubRepositoryNotFound + msg := fmt.Sprintf("no repositories found associated with CLA Group ID: %s that is enabled", claGroupID) + log.WithFields(f).Warn(msg) + return nil, &utils.GitHubRepositoryNotFound{ + Message: msg, + } } var repositories []*RepositoryDBModel @@ -336,9 +580,10 @@ func (r *repo) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, return convertModels(repositories), nil } -func (r *repo) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { +// GitHubGetRepositoriesByOrganizationName gets the repositories by organization name +func (r *Repository) GitHubGetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "GetRepositoriesByOrganizationName", + "functionName": "v1.repositories.repository.GitHubGetRepositoriesByOrganizationName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "gitHubOrgName": gitHubOrgName, } @@ -370,8 +615,11 @@ func (r *repo) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgN } if len(results.Items) == 0 { - log.WithFields(f).Warn("no repositories found matching the search criteria") - return nil, ErrGithubRepositoryNotFound + msg := fmt.Sprintf("no repositories found associated GitHub Organization: %s", gitHubOrgName) + log.WithFields(f).Debug(msg) + return nil, &utils.GitHubRepositoryNotFound{ + Message: msg, + } } var repositories []*RepositoryDBModel @@ -384,8 +632,8 @@ func (r *repo) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgN return convertModels(repositories), nil } -// GetCLAGroupRepositoriesGroupByOrgs returns a list of GH organizations by CLA Group - enabled flag indicates that we search the enabled repositories list -func (r repo) GetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepositoriesGroupByOrgs, error) { +// GitHubGetCLAGroupRepositoriesGroupByOrgs returns a list of GH organizations by CLA Group - enabled flag indicates that we search the enabled repositories list +func (r *Repository) GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepositoriesGroupByOrgs, error) { out := make([]*models.GithubRepositoriesGroupByOrgs, 0) outMap := make(map[string]*models.GithubRepositoriesGroupByOrgs) ghrepos, err := r.getProjectRepositories(ctx, projectID, enabled) @@ -406,32 +654,23 @@ func (r repo) GetCLAGroupRepositoriesGroupByOrgs(ctx context.Context, projectID return out, nil } -// List github repositories of project by external/salesforce project id -func (r repo) ListProjectRepositories(ctx context.Context, externalProjectID string, projectSFID string, enabled *bool) (*models.ListGithubRepositories, error) { +// GitHubListProjectRepositories lists GitHub repositories of project by external/salesforce project id +func (r *Repository) GitHubListProjectRepositories(ctx context.Context, projectSFID string, enabled *bool) (*models.GithubListRepositories, error) { f := logrus.Fields{ - "functionName": "ListProjectRepositories", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "externalProjectID": externalProjectID, - "projectSFID": projectSFID, - "enabled": utils.BoolValue(enabled), + "functionName": "v1.repositories.repository.GitHubListProjectRepositories", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "enabled": utils.BoolValue(enabled), } - var indexName string - out := &models.ListGithubRepositories{ + out := &models.GithubListRepositories{ List: make([]*models.GithubRepository, 0), } - var condition expression.KeyConditionBuilder - var filter expression.ConditionBuilder - if externalProjectID != "" { - condition = expression.Key("repository_sfdc_id").Equal(expression.Value(externalProjectID)) - indexName = SFDCRepositoryIndex - } else { - condition = expression.Key("project_sfid").Equal(expression.Value(projectSFID)) - indexName = ProjectSFIDRepositoryOrganizationNameIndex - } + condition := expression.Key("project_sfid").Equal(expression.Value(projectSFID)) // Add the enabled filter, if set + var filter expression.ConditionBuilder if enabled != nil { filter = expression.Name(repositoryEnabledColumn).Equal(expression.Value(enabled)) } @@ -447,7 +686,7 @@ func (r repo) ListProjectRepositories(ctx context.Context, externalProjectID str ProjectionExpression: expr.Projection(), FilterExpression: expr.Filter(), TableName: aws.String(r.repositoryTableName), - IndexName: aws.String(indexName), + IndexName: aws.String(RepositoryProjectSFIDOrganizationNameIndex), } results, err := r.dynamoDBClient.Query(queryInput) @@ -464,15 +703,15 @@ func (r repo) ListProjectRepositories(ctx context.Context, externalProjectID str return nil, err } for _, gr := range result { - out.List = append(out.List, gr.toModel()) + out.List = append(out.List, gr.ToGitHubModel()) } return out, nil } // getProjectRepositories returns an array of GH repositories for the specified project ID -func (r repo) getProjectRepositories(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepository, error) { +func (r *Repository) getProjectRepositories(ctx context.Context, projectID string, enabled bool) ([]*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "getProjectRepositories", + "functionName": "v1.repositories.repository.getProjectRepositories", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectID": projectID, "enabled": enabled, @@ -495,7 +734,7 @@ func (r repo) getProjectRepositories(ctx context.Context, projectID string, enab ProjectionExpression: expr.Projection(), FilterExpression: expr.Filter(), TableName: aws.String(r.repositoryTableName), - IndexName: aws.String(ProjectRepositoryIndex), + IndexName: aws.String(RepositoryProjectIndex), } results, err := r.dynamoDBClient.Query(queryInput) @@ -512,15 +751,15 @@ func (r repo) getProjectRepositories(ctx context.Context, projectID string, enab return nil, err } for _, gr := range result { - out = append(out, gr.toModel()) + out = append(out, gr.ToGitHubModel()) } return out, nil } // getRepositoriesByGithubOrg returns an array of GH repositories for the specified project ID -func (r repo) getRepositoriesByGithubOrg(ctx context.Context, githubOrgName string) ([]*models.GithubRepository, error) { +func (r *Repository) getRepositoriesByGithubOrg(ctx context.Context, githubOrgName string) ([]*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "getRepositoriesByGitHubOrg", + "functionName": "v1.repositories.repository.getRepositoriesByGitHubOrg", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "githubOrgName": githubOrgName, } @@ -557,15 +796,15 @@ func (r repo) getRepositoriesByGithubOrg(ctx context.Context, githubOrgName stri return nil, err } for _, gr := range result { - out = append(out, gr.toModel()) + out = append(out, gr.ToGitHubModel()) } return out, nil } -// GetRepositoryByGithubID fetches the repository model by its external github id -func (r repo) GetRepositoryByGithubID(ctx context.Context, externalID string, enabled bool) (*models.GithubRepository, error) { +// GitHubGetRepositoryByGithubID fetches the repository model by its external GitHub id +func (r *Repository) GitHubGetRepositoryByGithubID(ctx context.Context, externalID string, enabled bool) (*models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "GetRepositoryByGitHubID", + "functionName": "v1.repositories.repository.GetRepositoryByGitHubID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "externalID": externalID, "enabled": enabled, @@ -590,7 +829,7 @@ func (r repo) GetRepositoryByGithubID(ctx context.Context, externalID string, en ProjectionExpression: expr.Projection(), FilterExpression: expr.Filter(), TableName: aws.String(r.repositoryTableName), - IndexName: aws.String(ExternalRepositoryIndex), + IndexName: aws.String(RepositoryExternalIDIndex), } results, err := r.dynamoDBClient.Query(queryInput) @@ -600,39 +839,43 @@ func (r repo) GetRepositoryByGithubID(ctx context.Context, externalID string, en } var result *RepositoryDBModel if len(results.Items) == 0 { - return nil, ErrGithubRepositoryNotFound + msg := fmt.Sprintf("no repository found matching external repository ID: %s", externalID) + log.WithFields(f).Warn(msg) + return nil, &utils.GitHubRepositoryNotFound{ + Message: msg, + } } err = dynamodbattribute.UnmarshalMap(results.Items[0], &result) if err != nil { return nil, err } - return result.toModel(), nil + return result.ToGitHubModel(), nil } -func (r repo) enableGithubRepository(ctx context.Context, repositoryID string) error { +func (r *Repository) enableGithubRepository(ctx context.Context, repositoryID string) error { return r.setEnabledGithubRepository(ctx, repositoryID, true) } -func (r repo) enableGithubRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { +func (r *Repository) enableGithubRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { return r.setEnabledGithubRepositoryWithCLAGroupID(ctx, repositoryID, claGroupID, true) } -func (r repo) disableGithubRepository(ctx context.Context, repositoryID string) error { +func (r *Repository) disableGithubRepository(ctx context.Context, repositoryID string) error { return r.setEnabledGithubRepository(ctx, repositoryID, false) } // setEnabledGithubRepository updates the existing repository record by setting the enabled flag to false -func (r repo) setEnabledGithubRepository(ctx context.Context, repositoryID string, enabled bool) error { +func (r *Repository) setEnabledGithubRepository(ctx context.Context, repositoryID string, enabled bool) error { f := logrus.Fields{ - "functionName": "setEnabledGitHubRepository", + "functionName": "v1.repositories.repository.setEnabledGitHubRepository", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "repositoryID": repositoryID, "enabled": enabled, } - // Load the existing model - need to fetch the old note value, if available - existingModel, getErr := r.GetRepository(ctx, repositoryID) + // Load the existing model - need to fetch the old values, if available + existingModel, getErr := r.GitHubGetRepository(ctx, repositoryID) if getErr != nil { log.WithFields(f).WithError(getErr).Warn("unable to load repository by repository id") return getErr @@ -655,34 +898,37 @@ func (r repo) setEnabledGithubRepository(ctx context.Context, repositoryID strin _, now := utils.CurrentTime() log.WithFields(f).Debug("updating repository record") + + note := fmt.Sprintf("%s%s on %s", existingNote, enabledString, now) + + updateExpression := expression.Set(expression.Name(repositoryEnabledColumn), expression.Value(enabled)).Set(expression.Name("note"), expression.Value(note)).Set(expression.Name("date_modified"), expression.Value(now)) + + // delete project_sfid ,repository_sfdc_id and repository_project_id if enabled is false + if !enabled { + updateExpression = updateExpression.Remove(expression.Name("project_sfid")).Remove(expression.Name("repository_sfdc_id")).Remove(expression.Name("repository_project_id")) + } + + expr, exprErr := expression.NewBuilder().WithUpdate(updateExpression).Build() + if exprErr != nil { + log.WithFields(f).Warnf("error building expression for updating repository record, error: %v", exprErr) + return exprErr + } + _, err := r.dynamoDBClient.UpdateItem(&dynamodb.UpdateItemInput{ Key: map[string]*dynamodb.AttributeValue{ "repository_id": {S: aws.String(repositoryID)}, }, - ExpressionAttributeNames: map[string]*string{ - "#enabled": aws.String(repositoryEnabledColumn), - "#note": aws.String("note"), - "#dateModified": aws.String("date_modified"), - }, - ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ - ":enabledValue": { - BOOL: aws.Bool(enabled), - }, - ":noteValue": { - S: aws.String(fmt.Sprintf("%s%s on %s", existingNote, enabledString, now)), // Add to existing note, if set - }, - ":dateModifiedValue": { - S: aws.String(now), - }, - }, - UpdateExpression: aws.String("SET #enabled = :enabledValue, #note = :noteValue, #dateModified = :dateModifiedValue"), - TableName: aws.String(r.repositoryTableName), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + UpdateExpression: expr.Update(), + TableName: aws.String(r.repositoryTableName), }) + if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case dynamodb.ErrCodeConditionalCheckFailedException: - return errors.New("github repository entry does not exist or repository_sfdc_id does not match with specified project id") + return errors.New("github repository entry does not exist or *Repositorysitory_sfdc_id does not match with specified project id") } } log.WithFields(f).WithError(err).Warn("error disabling github repository") @@ -693,9 +939,9 @@ func (r repo) setEnabledGithubRepository(ctx context.Context, repositoryID strin } // setEnabledGithubRepositoryWithCLAGroupID updates the existing repository record by setting the enabled flag to false -func (r repo) setEnabledGithubRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string, enabled bool) error { +func (r *Repository) setEnabledGithubRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string, enabled bool) error { f := logrus.Fields{ - "functionName": "setEnabledGitHubRepositoryWithCLAGroupID", + "functionName": "v1.repositories.repository.setEnabledGitHubRepositoryWithCLAGroupID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "repositoryID": repositoryID, "claGroupID": claGroupID, @@ -703,7 +949,7 @@ func (r repo) setEnabledGithubRepositoryWithCLAGroupID(ctx context.Context, repo } // Load the existing model - need to fetch the old note value, if available - existingModel, getErr := r.GetRepository(ctx, repositoryID) + existingModel, getErr := r.GitHubGetRepository(ctx, repositoryID) if getErr != nil { log.WithFields(f).WithError(getErr).Warn("unable to load repository by repository id") return getErr @@ -757,7 +1003,7 @@ func (r repo) setEnabledGithubRepositoryWithCLAGroupID(ctx context.Context, repo if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case dynamodb.ErrCodeConditionalCheckFailedException: - return errors.New("github repository entry does not exist or repository_sfdc_id does not match with specified project id") + return errors.New("github repository entry does not exist or *Repositorysitory_sfdc_id does not match with specified project id") } } log.WithFields(f).WithError(err).Warn("error disabling github repository") @@ -768,16 +1014,16 @@ func (r repo) setEnabledGithubRepositoryWithCLAGroupID(ctx context.Context, repo } // setEnabledGithubRepository updates the existing repository record by setting the enabled flag to false -func (r repo) setClaGroupIDGithubRepository(ctx context.Context, repositoryID, claGroupID string) error { +func (r *Repository) setClaGroupIDGithubRepository(ctx context.Context, repositoryID, claGroupID string) error { f := logrus.Fields{ - "functionName": "setClaGroupIDGitHubRepository", + "functionName": "v1.repositories.repository.setClaGroupIDGitHubRepository", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "repositoryID": repositoryID, "claGroupID": claGroupID, } // Load the existing model - need to fetch the old note value, if available - existingModel, getErr := r.GetRepository(ctx, repositoryID) + existingModel, getErr := r.GitHubGetRepository(ctx, repositoryID) if getErr != nil { log.WithFields(f).WithError(getErr).Warn("unable to load repository by repository id") return getErr @@ -824,7 +1070,50 @@ func (r repo) setClaGroupIDGithubRepository(ctx context.Context, repositoryID, c if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case dynamodb.ErrCodeConditionalCheckFailedException: - return errors.New("github repository entry does not exist or repository_sfdc_id does not match with specified project id") + return errors.New("github repository entry does not exist or *Repositorysitory_sfdc_id does not match with specified project id") + } + } + log.WithFields(f).WithError(err).Warn("error disabling github repository") + return err + } + + return nil +} + +// GitHubSetRemoteDeletedRepository used to set remotely deleted flag on repository +func (r *Repository) GitHubSetRemoteDeletedRepository(ctx context.Context, repositoryID string, isDeleted bool, wasCLAEnforced bool) error { + f := logrus.Fields{ + "functionName": "v1.repositories.repository.GitHubSetRemoteDeletedRepository", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "repositoryID": repositoryID, + "isDeleted": isDeleted, + "wasCLAEnforced": wasCLAEnforced, + } + + _, now := utils.CurrentTime() + log.WithFields(f).Debug("Seting up remote deleted action.") + + updateExpression := expression.Set(expression.Name(repositoryRemoteDeletedColumn), expression.Value(isDeleted)).Set(expression.Name(repositoryWasCLAEnforcedColumn), expression.Value(wasCLAEnforced)).Set(expression.Name("date_modified"), expression.Value(now)) + + expr, exprErr := expression.NewBuilder().WithUpdate(updateExpression).Build() + if exprErr != nil { + log.WithFields(f).Warnf("error building expression for updating repository record, error: %v", exprErr) + return exprErr + } + _, err := r.dynamoDBClient.UpdateItem(&dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "repository_id": {S: aws.String(repositoryID)}, + }, + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + UpdateExpression: expr.Update(), + TableName: aws.String(r.repositoryTableName), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case dynamodb.ErrCodeConditionalCheckFailedException: + return errors.New("github repository entry does not exist or *Repositorysitory_sfdc_id does not match with specified project id") } } log.WithFields(f).WithError(err).Warn("error disabling github repository") diff --git a/cla-backend-go/repositories/service.go b/cla-backend-go/repositories/service.go index 641f6be7c..1bebb5175 100644 --- a/cla-backend-go/repositories/service.go +++ b/cla-backend-go/repositories/service.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" - "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" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" @@ -20,17 +20,18 @@ import ( project_service "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" ) -// Service contains functions of Github Repository service +// Service contains functions of GitHub Repository service type Service interface { AddGithubRepository(ctx context.Context, externalProjectID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) EnableRepository(ctx context.Context, repositoryID string) error EnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error DisableRepository(ctx context.Context, repositoryID string) error UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error - ListProjectRepositories(ctx context.Context, externalProjectID string, enabled *bool) (*models.ListGithubRepositories, error) + ListProjectRepositories(ctx context.Context, externalProjectID string, enabled *bool) (*models.GithubListRepositories, error) GetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) - GetRepositoryByProjectSFID(ctx context.Context, projectSFID string, enabled *bool) (*models.ListGithubRepositories, error) + GetRepositoryByProjectSFID(ctx context.Context, projectSFID string, enabled *bool) (*models.GithubListRepositories, error) GetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) + GetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*models.GithubRepository, error) DisableRepositoriesByProjectID(ctx context.Context, projectID string) (int, error) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string) ([]*models.GithubRepository, error) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) @@ -38,19 +39,19 @@ type Service interface { // GithubOrgRepo provide method to get github organization by name type GithubOrgRepo interface { - GetGithubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) - GetGithubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) - GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) + GetGitHubOrganizationByName(ctx context.Context, githubOrganizationName string) (*models.GithubOrganizations, error) + GetGitHubOrganization(ctx context.Context, githubOrganizationName string) (*models.GithubOrganization, error) + GetGitHubOrganizations(ctx context.Context, projectSFID string) (*models.GithubOrganizations, error) } type service struct { - repo Repository + repo RepositoryInterface ghOrgRepo GithubOrgRepo projectsClaGroupsRepo projects_cla_groups.Repository } // NewService creates a new githubOrganizations service -func NewService(repo Repository, ghOrgRepo GithubOrgRepo, pcgRepo projects_cla_groups.Repository) Service { +func NewService(repo RepositoryInterface, ghOrgRepo GithubOrgRepo, pcgRepo projects_cla_groups.Repository) Service { return &service{ repo: repo, ghOrgRepo: ghOrgRepo, @@ -60,7 +61,7 @@ func NewService(repo Repository, ghOrgRepo GithubOrgRepo, pcgRepo projects_cla_g // UpdateClaGroupID updates the claGroupID func (s *service) UpdateClaGroupID(ctx context.Context, repositoryID, claGroupID string) error { - return s.repo.UpdateClaGroupID(ctx, repositoryID, claGroupID) + return s.repo.GitHubUpdateClaGroupID(ctx, repositoryID, claGroupID) } func (s *service) AddGithubRepository(ctx context.Context, externalProjectID string, input *models.GithubRepositoryInput) (*models.GithubRepository, error) { @@ -88,7 +89,7 @@ func (s *service) AddGithubRepository(ctx context.Context, externalProjectID str return nil, projectErr } - org, err := s.ghOrgRepo.GetGithubOrganizationByName(ctx, utils.StringValue(input.RepositoryOrganizationName)) + org, err := s.ghOrgRepo.GetGitHubOrganizationByName(ctx, utils.StringValue(input.RepositoryOrganizationName)) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading github organization by name: %s", utils.StringValue(input.RepositoryOrganizationName)) return nil, err @@ -117,8 +118,8 @@ func (s *service) AddGithubRepository(ctx context.Context, externalProjectID str existingModel, err := s.GetRepositoryByName(ctx, utils.StringValue(input.RepositoryName)) if err != nil { // If not found - ok, otherwise we have a bigger problem - if errors.Is(err, ErrGithubRepositoryNotFound) { - log.WithFields(f).Debug("existing repository not found - will create") + if notFoundErr, ok := err.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).WithError(notFoundErr).Debug("existing repository not found - will create") } else { return nil, err } @@ -126,53 +127,71 @@ func (s *service) AddGithubRepository(ctx context.Context, externalProjectID str if existingModel != nil { log.WithFields(f).Debug("existing repository found - enabling it...") - err := s.EnableRepositoryWithCLAGroupID(ctx, existingModel.RepositoryID, utils.StringValue(input.RepositoryProjectID)) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem enabling repository") - return nil, err + enableErr := s.EnableRepositoryWithCLAGroupID(ctx, existingModel.RepositoryID, utils.StringValue(input.RepositoryProjectID)) + if enableErr != nil { + log.WithFields(f).WithError(enableErr).Warn("problem enabling repository") + return nil, enableErr } - return s.repo.GetRepository(ctx, existingModel.RepositoryID) + return s.repo.GitHubGetRepository(ctx, existingModel.RepositoryID) } // Doesn't exist - create it - return s.repo.AddGithubRepository(ctx, externalProjectID, projectSFID, input) + return s.repo.GitHubAddRepository(ctx, externalProjectID, projectSFID, input) } func (s *service) EnableRepository(ctx context.Context, repositoryID string) error { - return s.repo.EnableRepository(ctx, repositoryID) + return s.repo.GitHubEnableRepository(ctx, repositoryID) } func (s *service) EnableRepositoryWithCLAGroupID(ctx context.Context, repositoryID, claGroupID string) error { - return s.repo.EnableRepositoryWithCLAGroupID(ctx, repositoryID, claGroupID) + return s.repo.GitHubEnableRepositoryWithCLAGroupID(ctx, repositoryID, claGroupID) } func (s *service) DisableRepository(ctx context.Context, repositoryID string) error { - return s.repo.DisableRepository(ctx, repositoryID) + return s.repo.GitHubDisableRepository(ctx, repositoryID) } -func (s *service) ListProjectRepositories(ctx context.Context, externalProjectID string, enabled *bool) (*models.ListGithubRepositories, error) { - return s.repo.ListProjectRepositories(ctx, externalProjectID, "", enabled) +func (s *service) ListProjectRepositories(ctx context.Context, externalProjectID string, enabled *bool) (*models.GithubListRepositories, error) { + return s.repo.GitHubListProjectRepositories(ctx, externalProjectID, enabled) } func (s *service) GetRepository(ctx context.Context, repositoryID string) (*models.GithubRepository, error) { - return s.repo.GetRepository(ctx, repositoryID) + f := logrus.Fields{ + "functionName": "v1.repository.GitHubGetRepository", + "repositoryID": repositoryID, + } + log.WithFields(f).Debug("Searching for repository...") + ghRepo, err := s.repo.GitHubGetRepository(ctx, repositoryID) + if err != nil || ghRepo != nil { + log.WithFields(f).WithError(err).Debug("unable to get repository") + return nil, err + } + + log.WithFields(f).Debugf("Found repository : %+v ", ghRepo) + + return ghRepo, nil } -func (s *service) GetRepositoryByProjectSFID(ctx context.Context, projectSFID string, enabled *bool) (*models.ListGithubRepositories, error) { - return s.repo.ListProjectRepositories(ctx, "", projectSFID, enabled) +func (s *service) GetRepositoryByProjectSFID(ctx context.Context, projectSFID string, enabled *bool) (*models.GithubListRepositories, error) { + return s.repo.GitHubListProjectRepositories(ctx, projectSFID, enabled) } // GetRepositoryByName returns the repository by name: project-level/cla-project func (s *service) GetRepositoryByName(ctx context.Context, repositoryName string) (*models.GithubRepository, error) { - return s.repo.GetRepositoryByName(ctx, repositoryName) + return s.repo.GitHubGetRepositoryByName(ctx, repositoryName) +} + +// GetRepositoryByExternalID returns the repository by externalID +func (s *service) GetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*models.GithubRepository, error) { + return s.repo.GitHubGetRepositoryByExternalID(ctx, repositoryExternalID) } // DisableRepositoriesByProjectID disables the repositories by project ID func (s *service) DisableRepositoriesByProjectID(ctx context.Context, projectID string) (int, error) { var deleteErr error // Return the list of GitHub repositories by CLA Group for those that are currently enabled - ghOrgs, err := s.repo.GetCLAGroupRepositoriesGroupByOrgs(ctx, projectID, true) + ghOrgs, err := s.repo.GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx, projectID, true) if err != nil { return 0, err } @@ -180,7 +199,7 @@ func (s *service) DisableRepositoriesByProjectID(ctx context.Context, projectID log.Debugf("Deleting repositories for project :%s", projectID) for _, ghOrg := range ghOrgs { for _, item := range ghOrg.List { - deleteErr = s.repo.DisableRepository(ctx, item.RepositoryID) + deleteErr = s.repo.GitHubDisableRepository(ctx, item.RepositoryID) if deleteErr != nil { log.Warnf("Unable to remove repository: %s for project :%s error :%v", item.RepositoryID, projectID, deleteErr) } @@ -194,10 +213,10 @@ func (s *service) DisableRepositoriesByProjectID(ctx context.Context, projectID // GetRepositoriesByCLAGroup returns the list of repositories for the specified CLA Group func (s *service) GetRepositoriesByCLAGroup(ctx context.Context, claGroupID string) ([]*models.GithubRepository, error) { // Return the list of github repositories that are enabled - return s.repo.GetRepositoriesByCLAGroup(ctx, claGroupID, true) + return s.repo.GitHubGetRepositoriesByCLAGroup(ctx, claGroupID, true) } // GetRepositoriesByOrganizationName get repositories by organization name func (s *service) GetRepositoriesByOrganizationName(ctx context.Context, gitHubOrgName string) ([]*models.GithubRepository, error) { - return s.repo.GetRepositoriesByOrganizationName(ctx, gitHubOrgName) + return s.repo.GitHubGetRepositoriesByOrganizationName(ctx, gitHubOrgName) } diff --git a/cla-backend-go/serverless.yml b/cla-backend-go/serverless.yml index 3188aaac3..97689f079 100644 --- a/cla-backend-go/serverless.yml +++ b/cla-backend-go/serverless.yml @@ -1,28 +1,31 @@ +--- # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT service: cla-backend-go -frameworkVersion: '^2.11.0' +frameworkVersion: '^3.29.0' package: - # Exclude all first - selectively add in lambda functions - exclude: - - auth/** - - ./backend-aws-lambda - - dev.sh - - docs/** - - helpers/** - - Makefile - - .env/** - - .venv/** - - .git* - - .git/** - - .vscode/** - - .serverless-wsgi - - .pylintrc - - node_modules/** - - package-lock.json - - yarn.lock + # Exclude all first - selectively add in lambda functions, + # 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/**' + - '!.serverless-wsgi' + - '!.pylintrc' + - '!node_modules/**' + - '!package-lock.json' + - '!yarn.lock' custom: allowed_origins: ${file(./env.json):cla-allowed-origins-${opt:stage}, ssm:/cla-allowed-origins-${opt:stage}} @@ -34,7 +37,6 @@ custom: prune: automatic: true number: 3 - #userEventsSNSTopicARN: arn:aws:sns:${self:provider.region}:#{AWS::AccountId}:userservice-triggers-${self:provider.stage}-user-sns-topic ses_from_email: dev: admin@dev.lfcla.com staging: admin@staging.lfcla.com @@ -42,156 +44,169 @@ custom: provider: name: aws - provider: go1.x + runtime: go1.x stage: ${opt:stage} # EasyCLA v2 is deployed in us-east-2 to support Platform API GW and ACS region: us-east-2 timeout: 300 # 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" tracing: - lambda: true + lambda: true # optional, enables tracing for all functions (can be true (true equals 'Active') 'Active' or 'PassThrough') - # 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: - - ssm:GetParameter - Resource: - - "arn:aws:ssm:${self:provider.region}:#{AWS::AccountId}:parameter/cla-*" - - "arn:aws:ssm:${self:custom.dynamodb.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: - - 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:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-ccla-whitelist-requests" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-company-invites" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-session-store" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-store" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-user-permissions" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-metrics" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups" - - Effect: Allow - Action: - - dynamodb:Query - Resource: - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-ccla-whitelist-requests/index/company-id-project-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-ccla-whitelist-requests/index/ccla-approval-list-request-project-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/github-user-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/github-username-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/github-user-external-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/lf-username-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users/index/lf-email-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-project-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-project-sfid-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/project-signature-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/project-signature-date-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/reference-signature-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-project-reference-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-user-ccla-company-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/project-signature-external-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-company-signatory-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/reference-signature-search-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-project-id-type-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-company-initial-manager-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-signatures/index/signature-project-id-sigtype-signed-approved-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies/index/external-company-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies/index/company-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-companies/index/company-signing-entity-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/external-project-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/project-name-search-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/project-name-lower-search-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects/index/foundation-sfid-project-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/project-repository-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/repository-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/repository-organization-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/external-repository-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/sfdc-repository-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/project-sfid-repository-organization-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-repositories/index/project-sfid-repository-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs/index/github-org-sfid-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs/index/project-sfid-organization-name-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-github-orgs/index/organization-name-lower-search-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-company-invites/index/requested-company-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-type-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/user-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-id-external-project-id-event-epoch-time-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-project-id-event-time-epoch-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-date-and-contains-pii-event-time-epoch-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-sfid-foundation-sfid-event-time-epoch-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-sfid-project-id-event-time-epoch-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/company-id-event-type-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-events/index/event-foundation-sfid-event-time-epoch-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-metrics/index/metric-type-salesforce-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-company-project-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-external-company-project-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-project-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups/index/cla-group-id-index" - - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups/index/foundation-sfid-index" + 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-${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: + - ssm:GetParameter + Resource: + - "arn:aws:ssm:${self:provider.region}:${aws:accountId}:parameter/cla-*" + - "arn:aws:ssm:${self:custom.dynamodb.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: + - 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:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-ccla-whitelist-requests" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-cla-manager-requests" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-companies" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-company-invites" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-gerrit-instances" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-github-orgs" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-session-store" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-store" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-user-permissions" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-metrics" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects-cla-groups" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-gitlab-orgs" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-approvals" + - Effect: Allow + Action: + - dynamodb:Query + Resource: + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-ccla-whitelist-requests/index/company-id-project-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-ccla-whitelist-requests/index/ccla-approval-list-request-project-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/github-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/github-username-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/gitlab-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/gitlab-username-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/github-user-external-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/lf-username-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-users/index/lf-email-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-project-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-gerrit-instances/index/gerrit-project-sfid-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/project-signature-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/project-signature-date-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/reference-signature-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/signature-project-reference-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/signature-user-ccla-company-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/project-signature-external-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/signature-company-signatory-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/reference-signature-search-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/signature-project-id-type-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/signature-company-initial-manager-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-signatures/index/signature-project-id-sigtype-signed-approved-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-companies/index/external-company-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-companies/index/company-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-companies/index/company-signing-entity-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects/index/external-project-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects/index/project-name-search-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects/index/project-name-lower-search-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects/index/foundation-sfid-project-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/project-repository-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/repository-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/repository-organization-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/external-repository-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/sfdc-repository-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/project-sfid-repository-organization-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/project-sfid-repository-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-repositories/index/repository-type-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-github-orgs/index/github-org-sfid-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-github-orgs/index/project-sfid-organization-name-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-github-orgs/index/organization-name-lower-search-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-company-invites/index/requested-company-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-type-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/user-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/company-id-external-project-id-event-epoch-time-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-project-id-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-cla-group-id-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-date-and-contains-pii-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/company-sfid-foundation-sfid-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/company-sfid-project-id-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/company-id-event-type-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-foundation-sfid-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-company-sfid-event-data-lower-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/company-sfid-cla-group-id-event-time-epoch-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-events/index/event-project-sfid-event-type-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-metrics/index/metric-type-salesforce-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-company-project-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-external-company-project-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-project-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects-cla-groups/index/cla-group-id-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-projects-cla-groups/index/foundation-sfid-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-gitlab-orgs/index/*" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:${aws:accountId}:table/cla-${opt:stage}-approvals/index/*" environment: STAGE: ${self:provider.stage} @@ -199,33 +214,36 @@ provider: REGION: us-east-2 # Currently, we use DynamoDB in the us-east-1 region 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} + GH_APP_WEBHOOK_SECRET: ${file(./env.json):gh-app-webhook-secret, ssm:/cla-gh-app-webhook-secret-${opt:stage}} + GH_APP_ID: ${file(./env.json):gh-app-id, ssm:/cla-gh-app-id-${opt:stage}} + GH_OAUTH_CLIENT_ID: ${file(./env.json):gh-oauth-client-id, ssm:/cla-gh-oauth-client-id-${opt:stage}} + GH_OAUTH_SECRET: ${file(./env.json):gh-oauth-secret, ssm:/cla-gh-oauth-secret-${opt:stage}} + GITHUB_OAUTH_TOKEN: ${file(./env.json):gh-access-token, ssm:/cla-gh-access-token-${opt: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_DOMAIN: ${file(./env.json):auth0-domain, ssm:/cla-auth0-domain-${opt:stage}} + AUTH0_CLIENT_ID: ${file(./env.json):auth0-clientId, ssm:/cla-auth0-clientId-${opt:stage}} 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} + SF_INSTANCE_URL: ${file(./env.json):sf-instance-url, ssm:/cla-sf-instance-url-${opt:stage}} + SF_CLIENT_ID: ${file(./env.json):sf-client-id, ssm:/cla-sf-consumer-key-${opt:stage}} + SF_CLIENT_SECRET: ${file(./env.json):sf-client-secret, ssm:/cla-sf-consumer-secret-${opt:stage}} + SF_USERNAME: ${file(./env.json):sf-username, ssm:/cla-sf-username-${opt:stage}} + SF_PASSWORD: ${file(./env.json):sf-password, ssm:/cla-sf-password-${opt:stage}} DOCRAPTOR_API_KEY: ${file(./env.json):doc-raptor-api-key, ssm:/cla-doc-raptor-api-key-${opt:stage}} DOCUSIGN_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}} + DOCUSIGN_AUTH_SERVER: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-auth-server-${opt:stage}} + DOCUSIGN_USER_ID: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-user-id-${opt:stage}} + DOCUSIGN_ACCOUNT_ID: ${file(./env.json):docusign-account-id, ssm:/cla-docusign-account-id-${opt:stage}} CLA_API_BASE: ${file(./env.json):cla-api-base, ssm:/cla-api-base-${opt:stage}} CLA_CONTRIBUTOR_BASE: ${file(./env.json):cla-contributor-base, ssm:/cla-contributor-base-${opt:stage}} CLA_CONTRIBUTOR_V2_BASE: ${file(./env.json):cla-contributor-v2-base, ssm:/cla-contributor-v2-base-${opt:stage}} 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} + CLA_SIGNATURE_FILES_BUCKET: ${file(./env.json):cla-signature-files-bucket, ssm:/cla-signature-files-bucket-${opt:stage}} + CLA_BUCKET_LOGO_URL: ${file(./env.json):cla-logo-url, ssm:/cla-logo-url-${opt:stage}} SES_SENDER_EMAIL_ADDRESS: ${file(./env.json):cla-ses-sender-email-address, ssm:/cla-ses-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}} @@ -248,6 +266,8 @@ provider: # https://github.com/pypa/setuptools/issues/2350 and # https://github.com/pypa/setuptools/issues/2232 SETUPTOOLS_USE_DISTUTILS: stdlib + # Turn on USER_AUTH_TRACING to see additional debug of user scopes for the authenticated users - output is verbose + USER_AUTH_TRACING: true stackTags: Name: ${self:service} @@ -255,7 +275,7 @@ provider: Project: "EasyCLA" Product: "EasyCLA" ManagedBy: "Serverless CloudFormation" - SericeType: "Product" + ServiceType: "Product" Service: ${self:service} ServiceRole: "Backend" ProgrammingPlatform: Go @@ -266,7 +286,7 @@ provider: Project: "EasyCLA" Product: "EasyCLA" ManagedBy: "Serverless CloudFormation" - SericeType: "Product" + ServiceType: "Product" Service: ${self:service} ServiceRole: "Backend" ProgrammingPlatform: Go @@ -274,7 +294,6 @@ provider: plugins: - 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 @@ -289,25 +308,11 @@ functions: # Provide name, otherwise the default will be something like: ${self:service}-${stage}-api # which is fine, but we need to add the name to the platform api-gw config to make lambda-to-lambda invoke call # so, setting this allows us to always know the name of the lambda (hard-coded now, which ignores the stage identifier in the name) - name: ${self:service}-api + name: ${self:service}-api-v4-lambda # must match lfx-api-gw lambda name description: "EasyCLA v2 API" runtime: go1.x - handler: backend-aws-lambda + handler: 'bin/backend-aws-lambda' package: individually: true - include: - - ./backend-aws-lambda - - # easyClaUserSubscribe: - # name: easy-cla-user-subscribe - # runtime: go1.x - # description: Update easycla user data to user object in dynamodb - # handler: user-subscribe-lambda - # package: - # individually: true - # include: - # - ./user-subscribe-lambda - # reservedConcurrency: 5 - # events: - # - sns: - # arn: ${self:custom.userEventsSNSTopicARN} + patterns: + - 'bin/backend-aws-lambda' diff --git a/cla-backend-go/service_discovery/service_discovery.go b/cla-backend-go/service_discovery/service_discovery.go deleted file mode 100644 index 0eddb94d3..000000000 --- a/cla-backend-go/service_discovery/service_discovery.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package service_discovery - -import ( - "github.com/communitybridge/easycla/cla-backend-go/company" - "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/project" - "github.com/communitybridge/easycla/cla-backend-go/signatures" - "github.com/communitybridge/easycla/cla-backend-go/users" - v2CompanyService "github.com/communitybridge/easycla/cla-backend-go/v2/company" - v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project" -) - -// ServiceDiscovery interface -type ServiceDiscovery interface { - SetEventService(eventService events.Service) - GetEventService() events.Service - SetUserService(userService users.Service) - GetUserService() users.Service - SetV1CompanyService(companyService company.IService) - GetV1CompanyService() company.IService - SetV2CompanyService(companyService v2CompanyService.Service) - GetV2CompanyService() v2CompanyService.Service - GetV1ProjectService() project.Service - GetV2ProjectService() v2ProjectService.Service - GetV1SignatureService() signatures.SignatureService -} - -type service struct { - eventService events.Service - userService users.Service - v1CompanyService company.IService - v2CompanyService v2CompanyService.Service - v1ProjectService project.Service - v2ProjectService v2ProjectService.Service - v1SignatureService signatures.SignatureService -} - -// NewServiceDiscovery creates a new whitelist service -func NewServiceDiscovery() ServiceDiscovery { - return service{} -} - -// GetEventService sets the event service reference -func (s service) SetEventService(eventService events.Service) { - s.eventService = eventService -} - -// GetEventService returns a reference to the the event service -func (s service) GetEventService() events.Service { - return s.eventService -} - -// SetUserService sets the user service reference -func (s service) SetUserService(userService users.Service) { - s.userService = userService -} - -// GetUserService returns a reference to the the user service -func (s service) GetUserService() users.Service { - return s.userService -} - -// SetV1CompanyService sets the v1 company service reference -func (s service) SetV1CompanyService(companyService company.IService) { - s.v1CompanyService = companyService -} - -// GetV1CompanyService returns a reference to the the v1 company service -func (s service) GetV1CompanyService() company.IService { - return s.v1CompanyService -} - -// SetV2CompanyService sets the v1 company service reference -func (s service) SetV2CompanyService(companyService v2CompanyService.Service) { - s.v2CompanyService = companyService -} - -// GetV2CompanyService returns a reference to the the v2 company service -func (s service) GetV2CompanyService() v2CompanyService.Service { - return s.v2CompanyService -} - -// GetV1ProjectService returns a reference to the the v1 project service -func (s service) GetV1ProjectService() project.Service { return s.v1ProjectService } - -// GetV2ProjectService returns a reference to the the v2 project service -func (s service) GetV2ProjectService() v2ProjectService.Service { return s.v2ProjectService } - -// GetV1SignatureService returns a reference to the the v1 signature service -func (s service) GetV1SignatureService() signatures.SignatureService { return s.v1SignatureService } diff --git a/cla-backend-go/signatures/constants.go b/cla-backend-go/signatures/constants.go new file mode 100644 index 000000000..1e2de9f07 --- /dev/null +++ b/cla-backend-go/signatures/constants.go @@ -0,0 +1,28 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +// SignatureEmailApprovalListColumn is the name of the signature column for the email approval list +const SignatureEmailApprovalListColumn = "email_whitelist" // TODO: rename column to email_approval_list + +// SignatureDomainApprovalListColumn is the name of the signature column for the domain approval list +const SignatureDomainApprovalListColumn = "domain_whitelist" // TODO: rename column to domain_approval_list + +// SignatureGitHubUsernameApprovalListColumn is the name of the signature column for the GitHub username approval list +const SignatureGitHubUsernameApprovalListColumn = "github_whitelist" // TODO: rename column to github_username_approval_list + +// SignatureGitHubOrgApprovalListColumn is the name of the signature column for the GitHub organization approval list +const SignatureGitHubOrgApprovalListColumn = "github_org_whitelist" // TODO: rename column to github_org_approval_list + +// SignatureGitlabUsernameApprovalListColumn is the name of the signature column for gitlab username approval lists +const SignatureGitlabUsernameApprovalListColumn = "gitlab_username_approval_list" + +// SignatureGitlabOrgApprovalListColumn is the name of the signature column for gitlab organization approval lists +const SignatureGitlabOrgApprovalListColumn = "gitlab_org_approval_list" // nolint G101: Potential hardcoded credentials (gosec) + +// SignatureUserGitHubUsername is the name of the signature column for user gitlab username +const SignatureUserGitHubUsername = "user_github_username" + +// SignatureUserGitlabUsername is the name of the signature column for user gitlab username +const SignatureUserGitlabUsername = "user_gitlab_username" diff --git a/cla-backend-go/signatures/converters.go b/cla-backend-go/signatures/converters.go new file mode 100644 index 000000000..5b731bb36 --- /dev/null +++ b/cla-backend-go/signatures/converters.go @@ -0,0 +1,399 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +import ( + "context" + "strconv" + "strings" + "sync" + + "github.com/aws/aws-sdk-go/aws" + "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/v1/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +// buildProjectSignatureModels converts the response model into a response data model +func (repo repository) buildProjectSignatureModels(ctx context.Context, results *dynamodb.QueryOutput, claGroupID string, loadACLDetails bool) ([]*models.Signature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.converters.buildProjectSignatureModels", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + } + var sigs []*models.Signature + + // The DB signature model + var dbSignatures []ItemSignature + + err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling signatures from database for cla group ID: %s, error: %v", + claGroupID, err) + return nil, err + } + + var wg sync.WaitGroup + wg.Add(len(dbSignatures)) + for _, dbSignature := range dbSignatures { + + // Set the signature type in the response + var claType = "" + // Corporate Signature + if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeCompany && dbSignature.SignatureType == utils.SignatureTypeCCLA { + claType = utils.ClaTypeCCLA + } + // Employee Signature + if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeUser && dbSignature.SignatureType == utils.SignatureTypeCLA && dbSignature.SignatureUserCompanyID != "" { + claType = utils.ClaTypeECLA + } + + // Individual Signature + if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeUser && dbSignature.SignatureType == utils.SignatureTypeCLA && dbSignature.SignatureUserCompanyID == "" { + claType = utils.ClaTypeICLA + } + + // Use the signedOn field if possible, for older signatures that are missing it, use the date created value as the default/fallback + signedOn := dbSignature.DateCreated + if dbSignature.SignedOn != "" { + signedOn = dbSignature.SignedOn + } + signedOn = utils.FormatTimeString(signedOn) + + sig := &models.Signature{ + SignatureID: dbSignature.SignatureID, + ClaType: claType, + SignatureCreated: dbSignature.DateCreated, + SignatureModified: dbSignature.DateModified, + SignatureType: dbSignature.SignatureType, + SignatureReferenceID: dbSignature.SignatureReferenceID, + SignatureReferenceName: dbSignature.SignatureReferenceName, + SignatureReferenceNameLower: dbSignature.SignatureReferenceNameLower, + SignatureSigned: dbSignature.SignatureSigned, + SignatureApproved: dbSignature.SignatureApproved, + SignatureDocumentMajorVersion: strconv.Itoa(dbSignature.SignatureDocumentMajorVersion), + SignatureDocumentMinorVersion: strconv.Itoa(dbSignature.SignatureDocumentMinorVersion), + Version: strconv.Itoa(dbSignature.SignatureDocumentMajorVersion) + "." + strconv.Itoa(dbSignature.SignatureDocumentMinorVersion), + SignatureReferenceType: dbSignature.SignatureReferenceType, + ProjectID: dbSignature.SignatureProjectID, + Created: dbSignature.DateCreated, + Modified: dbSignature.DateModified, + EmailApprovalList: utils.GetNilSliceIfEmpty(dbSignature.EmailApprovalList), + DomainApprovalList: utils.GetNilSliceIfEmpty(dbSignature.EmailDomainApprovalList), + GithubUsernameApprovalList: utils.GetNilSliceIfEmpty(dbSignature.GitHubUsernameApprovalList), + GithubOrgApprovalList: utils.GetNilSliceIfEmpty(dbSignature.GitHubOrgApprovalList), + GitlabUsernameApprovalList: utils.GetNilSliceIfEmpty(dbSignature.GitlabUsernameApprovalList), + GitlabOrgApprovalList: utils.GetNilSliceIfEmpty(dbSignature.GitlabOrgApprovalList), + UserName: dbSignature.UserName, + UserLFID: dbSignature.UserLFUsername, + UserGHID: dbSignature.UserGithubID, + UserGHUsername: dbSignature.UserGithubUsername, + UserGitlabID: dbSignature.UserGitlabID, + UserGitlabUsername: dbSignature.UserGitlabUsername, + SignedOn: signedOn, + SignatoryName: dbSignature.SignatoryName, + UserDocusignName: dbSignature.UserDocusignName, + UserDocusignDateSigned: dbSignature.UserDocusignDateSigned, + AutoCreateECLA: dbSignature.AutoCreateECLA, + SignatureSignURL: dbSignature.SignatureSignURL, + SignatureCallbackURL: dbSignature.SignatureCallbackURL, + SignatureReturnURL: dbSignature.SignatureReturnURL, + SignatureReturnURLType: dbSignature.SignatureReturnURLType, + SignatureEnvelopeID: dbSignature.SignatureEnvelopeID, + } + + sigs = append(sigs, sig) + go func(sigModel *models.Signature, signatureUserCompanyID string, sigACL []string) { + defer wg.Done() + var companyName = "" + var companySigningEntityName = "" + var userName = "" + var userLFID = "" + var userGHID = "" + var userGHUsername = "" + var swg sync.WaitGroup + swg.Add(2) + + go func() { + defer swg.Done() + if sigModel.SignatureReferenceType == utils.SignatureReferenceTypeUser { + userModel, userErr := repo.usersRepo.GetUser(sigModel.SignatureReferenceID) + if userErr != nil || userModel == nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user for signature: %s with reference type: %s using signature reference id: %s", + sigModel.SignatureID, sigModel.SignatureReferenceType, sigModel.SignatureReferenceID) + } else { + userName = userModel.Username + userLFID = userModel.LfUsername + userGHID = userModel.GithubID + userGHUsername = userModel.GithubUsername + } + + if signatureUserCompanyID != "" { + dbCompanyModel, companyErr := repo.companyRepo.GetCompany(ctx, signatureUserCompanyID) + if companyErr != nil { + log.WithFields(f).WithError(companyErr).Warnf("unable to lookup company record for signature: %s with reference type: %s using signature user company id: %s", + sigModel.SignatureID, sigModel.SignatureReferenceType, signatureUserCompanyID) + } else { + companyName = dbCompanyModel.CompanyName + companySigningEntityName = dbCompanyModel.SigningEntityName + } + } + } else if sigModel.SignatureReferenceType == utils.SignatureReferenceTypeCompany { + dbCompanyModel, companyErr := repo.companyRepo.GetCompany(ctx, sigModel.SignatureReferenceID) + if companyErr != nil { + log.WithFields(f).WithError(companyErr).Warnf("unable to lookup company record for signature: %s with reference type: %s using signature reference id: %s", + sigModel.SignatureID, sigModel.SignatureReferenceType, sigModel.SignatureReferenceID) + } else { + companyName = dbCompanyModel.CompanyName + companySigningEntityName = dbCompanyModel.SigningEntityName + } + } + }() + + var signatureACL []models.User + go func() { + defer swg.Done() + for _, userName := range sigACL { + log.WithFields(f).Debugf("looking up user by user name: %s", userName) + if loadACLDetails { + userModel, userErr := repo.usersRepo.GetUserByUserName(userName, true) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by username: %s in ACL for signature: %s", userName, sigModel.SignatureID) + } else { + if userModel == nil { + log.WithFields(f).Warnf("unable to lookup user by username: %s in ACL for signature: %s", userName, sigModel.SignatureID) + } else { + signatureACL = append(signatureACL, *userModel) + } + } + } else { + signatureACL = append(signatureACL, models.User{LfUsername: userName}) + } + } + }() + swg.Wait() + sigModel.CompanyName = companyName + sigModel.SigningEntityName = companySigningEntityName + sigModel.UserName = userName + sigModel.UserLFID = userLFID + sigModel.UserGHID = userGHID + sigModel.UserGHUsername = userGHUsername + sigModel.SignatureACL = signatureACL + }(sig, dbSignature.SignatureUserCompanyID, dbSignature.SignatureACL) + } + wg.Wait() + return sigs, nil +} + +// buildProjectSignatureSummaryModels converts the response model into a signature summary model +func (repo repository) buildProjectSignatureSummaryModels(ctx context.Context, results *dynamodb.QueryOutput, projectID string) ([]*models.SignatureSummary, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.converters.buildProjectSignatureSummaryModels", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": projectID, + } + var sigs []*models.SignatureSummary + + // The DB signature model + var dbSignatures []ItemSignature + + err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling signatures from database for project: %s, error: %v", + projectID, err) + return nil, err + } + + var wg sync.WaitGroup + wg.Add(len(dbSignatures)) + for _, dbSignature := range dbSignatures { + + // Set the signature type in the response + var claType = "" + // Corporate Signature + if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeCompany && dbSignature.SignatureType == utils.SignatureTypeCCLA { + claType = utils.ClaTypeCCLA + } + // Employee Signature + if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeUser && dbSignature.SignatureType == utils.SignatureTypeCLA && dbSignature.SignatureUserCompanyID != "" { + claType = utils.ClaTypeECLA + } + + // Individual Signature + if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeUser && dbSignature.SignatureType == utils.SignatureTypeCLA && dbSignature.SignatureUserCompanyID == "" { + claType = utils.ClaTypeICLA + } + + sig := &models.SignatureSummary{ + SignatureID: dbSignature.SignatureID, + ClaType: claType, + SignatureType: dbSignature.SignatureType, + SignatureReferenceID: dbSignature.SignatureReferenceID, + SignatureReferenceName: dbSignature.SignatureReferenceName, + SignatureReferenceNameLower: dbSignature.SignatureReferenceNameLower, + SignatureSigned: dbSignature.SignatureSigned, + SignatureApproved: dbSignature.SignatureApproved, + SignatureReferenceType: dbSignature.SignatureReferenceType, + ProjectID: dbSignature.SignatureProjectID, + SignedOn: dbSignature.SignedOn, + SignatoryName: dbSignature.SignatoryName, + UserDocusignName: dbSignature.UserDocusignName, + UserDocusignDateSigned: dbSignature.UserDocusignDateSigned, + } + + sigs = append(sigs, sig) + go func(sigModel *models.SignatureSummary, signatureUserCompanyID string) { + defer wg.Done() + var companyName = "" + var companySigningEntityName = "" + var swg sync.WaitGroup + swg.Add(1) + + go func() { + defer swg.Done() + if sigModel.SignatureReferenceType == "user" { + if signatureUserCompanyID != "" { + dbCompanyModel, companyErr := repo.companyRepo.GetCompany(ctx, signatureUserCompanyID) + if companyErr != nil { + log.WithFields(f).WithError(companyErr).Warnf("unable to lookup company record for signature: %s with reference type: %s using signature user company id: %s", + sigModel.SignatureID, sigModel.SignatureReferenceType, signatureUserCompanyID) + } else { + companyName = dbCompanyModel.CompanyName + companySigningEntityName = dbCompanyModel.SigningEntityName + } + } + } else if sigModel.SignatureReferenceType == "company" { + dbCompanyModel, companyErr := repo.companyRepo.GetCompany(ctx, sigModel.SignatureReferenceID) + if companyErr != nil { + log.WithFields(f).WithError(companyErr).Warnf("unable to lookup company record for signature: %s with reference type: %s using signature reference id: %s", + sigModel.SignatureID, sigModel.SignatureReferenceType, sigModel.SignatureReferenceID) + } else { + companyName = dbCompanyModel.CompanyName + companySigningEntityName = dbCompanyModel.SigningEntityName + } + } + }() + swg.Wait() + + sigModel.CompanyName = companyName + sigModel.SigningEntityName = companySigningEntityName + }(sig, dbSignature.SignatureUserCompanyID) + } + + wg.Wait() + return sigs, nil +} + +// buildResponse is a helper function which converts a database model to a GitHub organization response model +func buildResponse(items []*dynamodb.AttributeValue) []models.GithubOrg { + // Convert to a response model + var orgs []models.GithubOrg + for _, org := range items { + selected := true + orgs = append(orgs, models.GithubOrg{ + ID: org.S, + Selected: &selected, + }) + } + + return orgs +} + +// buildApprovalAttributeList builds the updated approval list based on the added and removed values +func buildApprovalAttributeList(ctx context.Context, existingList, addEntries, removeEntries []string) *dynamodb.AttributeValue { + f := logrus.Fields{ + "functionName": "buildApprovalAttributeList", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + var updatedList []string + log.WithFields(f).Debugf("buildApprovalAttributeList - existing: %+v, add entries: %+v, remove entries: %+v", + existingList, addEntries, removeEntries) + + // Add the existing entries to our response + for _, value := range existingList { + // No duplicates allowed + if !utils.StringInSlice(value, updatedList) { + log.WithFields(f).Debugf("buildApprovalAttributeList - adding existing entry: %s", value) + updatedList = append(updatedList, strings.TrimSpace(value)) + } else { + log.WithFields(f).Debugf("buildApprovalAttributeList - skipping existing entry: %s", value) + } + } + + // For all the new values... + for _, value := range addEntries { + // No duplicates allowed + if !utils.StringInSlice(value, updatedList) { + log.WithFields(f).Debugf("buildApprovalAttributeList - adding new entry: %s", value) + updatedList = append(updatedList, strings.TrimSpace(value)) + } else { + log.WithFields(f).Debugf("buildApprovalAttributeList - skipping new entry: %s", value) + } + } + + // Remove the items + log.WithFields(f).Debugf("buildApprovalAttributeList - before: %+v - removing entries: %+v", updatedList, removeEntries) + updatedList = utils.RemoveItemsFromList(updatedList, removeEntries) + log.WithFields(f).Debugf("buildApprovalAttributeList - after: %+v - removing entries: %+v", updatedList, removeEntries) + + // Remove any duplicates - shouldn't have any if checked before adding + log.WithFields(f).Debugf("buildApprovalAttributeList - before: %+v - removing duplicates", updatedList) + updatedList = utils.RemoveDuplicates(updatedList) + log.WithFields(f).Debugf("buildApprovalAttributeList - after: %+v - removing duplicates", updatedList) + + // Convert to the response type + var responseList []*dynamodb.AttributeValue + for _, value := range updatedList { + responseList = append(responseList, &dynamodb.AttributeValue{S: aws.String(value)}) + } + + return &dynamodb.AttributeValue{L: responseList} +} + +// buildCompanyIDList is a helper function to convert the DB response models into a simple list of company IDs +func (repo repository) buildCompanyIDList(ctx context.Context, results *dynamodb.QueryOutput) ([]SignatureCompanyID, error) { + f := logrus.Fields{ + "functionName": "buildCompanyIDList", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + var response []SignatureCompanyID + + // The DB signature model + var dbSignatures []ItemSignature + err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling signatures from database, error: %v", err) + return nil, err + } + + // Loop and extract the company ID (signature_reference_id) value + for _, item := range dbSignatures { + // Lookup the company by ID - try to get more information like the external ID and name + companyModel, companyLookupErr := repo.companyRepo.GetCompany(ctx, item.SignatureReferenceID) + // Start building a model for this entry in the list + signatureCompanyID := SignatureCompanyID{ + SignatureID: item.SignatureID, + CompanyID: item.SignatureReferenceID, + } + + if companyLookupErr != nil || companyModel == nil { + log.WithFields(f).Warnf("problem looking up company using id: %s, error: %+v", + item.SignatureReferenceID, companyLookupErr) + response = append(response, signatureCompanyID) + } else { + if companyModel.CompanyExternalID != "" { + signatureCompanyID.CompanySFID = companyModel.CompanyExternalID + } + if companyModel.CompanyName != "" { + signatureCompanyID.CompanyName = companyModel.CompanyName + } + response = append(response, signatureCompanyID) + } + } + + return response, nil +} diff --git a/cla-backend-go/signatures/dbmodels.go b/cla-backend-go/signatures/dbmodels.go index 021fa80c9..262acfff2 100644 --- a/cla-backend-go/signatures/dbmodels.go +++ b/cla-backend-go/signatures/dbmodels.go @@ -5,34 +5,46 @@ package signatures // ItemSignature database model type ItemSignature struct { - SignatureID string `json:"signature_id"` - DateCreated string `json:"date_created"` - DateModified string `json:"date_modified"` - SignatureApproved bool `json:"signature_approved"` + SignatureID string `json:"signature_id"` // No omitempty, always included + DateCreated string `json:"date_created,omitempty"` + DateModified string `json:"date_modified,omitempty"` + SignatureApproved bool `json:"signature_approved,omitempty"` SignatureSigned bool `json:"signature_signed"` - SignatureDocumentMajorVersion string `json:"signature_document_major_version"` - SignatureDocumentMinorVersion string `json:"signature_document_minor_version"` - SignatureReferenceID string `json:"signature_reference_id"` - SignatureReferenceName string `json:"signature_reference_name"` - SignatureReferenceNameLower string `json:"signature_reference_name_lower"` - SignatureProjectID string `json:"signature_project_id"` - SignatureReferenceType string `json:"signature_reference_type"` - SignatureType string `json:"signature_type"` - SignatureUserCompanyID string `json:"signature_user_ccla_company_id"` - EmailWhitelist []string `json:"email_whitelist"` - DomainWhitelist []string `json:"domain_whitelist"` - GitHubWhitelist []string `json:"github_whitelist"` - GitHubOrgWhitelist []string `json:"github_org_whitelist"` - SignatureACL []string `json:"signature_acl"` - UserGithubUsername string `json:"user_github_username"` - UserLFUsername string `json:"user_lf_username"` - UserName string `json:"user_name"` - UserEmail string `json:"user_email"` - SigtypeSignedApprovedID string `json:"sigtype_signed_approved_id"` - SignedOn string `json:"signed_on"` - SignatoryName string `json:"signatory_name"` - UserDocusignName string `json:"user_docusign_name"` - UserDocusignDateSigned string `json:"user_docusign_date_signed"` + SignatureDocumentMajorVersion int `json:"signature_document_major_version,omitempty"` + SignatureDocumentMinorVersion int `json:"signature_document_minor_version,omitempty"` + SignatureSignURL string `json:"signature_sign_url,omitempty"` + SignatureReturnURL string `json:"signature_return_url,omitempty"` + SignatureReturnURLType string `json:"signature_return_url_type,omitempty"` + SignatureCallbackURL string `json:"signature_callback_url,omitempty"` + SignatureReferenceID string `json:"signature_reference_id,omitempty"` + SignatureReferenceName string `json:"signature_reference_name,omitempty"` + SignatureReferenceNameLower string `json:"signature_reference_name_lower,omitempty"` + SignatureProjectID string `json:"signature_project_id,omitempty"` + SignatureReferenceType string `json:"signature_reference_type,omitempty"` + SignatureType string `json:"signature_type,omitempty"` + SignatureEnvelopeID string `json:"signature_envelope_id,omitempty"` + SignatureUserCompanyID string `json:"signature_user_ccla_company_id,omitempty"` + EmailApprovalList []string `json:"email_whitelist,omitempty"` + EmailDomainApprovalList []string `json:"domain_whitelist,omitempty"` + GitHubUsernameApprovalList []string `json:"github_whitelist,omitempty"` + GitHubOrgApprovalList []string `json:"github_org_whitelist,omitempty"` + GitlabUsernameApprovalList []string `json:"gitlab_username_approval_list,omitempty"` + GitlabOrgApprovalList []string `json:"gitlab_org_approval_list,omitempty"` + SignatureACL []string `json:"signature_acl,omitempty"` + UserGithubID string `json:"user_github_id,omitempty"` + UserGithubUsername string `json:"user_github_username,omitempty"` + UserGitlabID string `json:"user_gitlab_id,omitempty"` + UserGitlabUsername string `json:"user_gitlab_username,omitempty"` + UserLFUsername string `json:"user_lf_username,omitempty"` + UserName string `json:"user_name,omitempty"` + UserEmail string `json:"user_email,omitempty"` + SigtypeSignedApprovedID string `json:"sigtype_signed_approved_id,omitempty"` + SignedOn string `json:"signed_on,omitempty"` + SignatoryName string `json:"signatory_name,omitempty"` + UserDocusignName string `json:"user_docusign_name,omitempty"` + UserDocusignDateSigned string `json:"user_docusign_date_signed,omitempty"` + AutoCreateECLA bool `json:"auto_create_ecla,omitempty"` + UserDocusignRawXML string `json:"user_docusign_raw_xml,omitempty"` } // DBManagersModel is a database model for only the ACL/Manager column @@ -46,3 +58,10 @@ type DBSignatureUsersModel struct { SignatureID string `json:"signature_id"` UserID string `json:"signature_reference_id"` } + +// DBSignatureMetadata is a database model for the key and value fields for a given signature +type DBSignatureMetadata struct { + Key string `json:"key"` + Expire int64 `json:"expire"` + Value string `json:"value"` +} diff --git a/cla-backend-go/signatures/email.go b/cla-backend-go/signatures/email.go new file mode 100644 index 000000000..7677e2796 --- /dev/null +++ b/cla-backend-go/signatures/email.go @@ -0,0 +1,231 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package signatures + +import ( + "fmt" + "strings" + + "github.com/LF-Engineering/lfx-kit/auth" + "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/utils" + "github.com/sirupsen/logrus" +) + +// ClaManagerInfoParams represents the CLAManagerInfo used inside of the Email Templates +type ClaManagerInfoParams struct { + Username string + Email string +} + +// InvalidateSignatureTemplateParams representing params when invalidating icla/ecla +type InvalidateSignatureTemplateParams struct { + RecipientName string + ClaType string + ClaManager string + RemovalCriteria string + ProjectName string + ProjectManager string + CLAManagers []ClaManagerInfoParams + CLaManager string + CLAGroupName string + Company string +} + +const ( + //InvalidateCCLAICLASignatureTemplateName is email template for InvalidateSignatureTemplate + InvalidateCCLAICLASignatureTemplateName = "InvalidateSignatureTemplate" + //InvalidateCCLAICLASignatureTemplate ... + InvalidateCCLAICLASignatureTemplate = ` +

    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:

    +
      + {{range .CLAManagers}} +
    • {{.Username}} {{.Email}}
    • + {{end}} +
    + ` + + //InvalidateCCLASignatureTemplateName is email template upon approval list removal for ccla use case + InvalidateCCLASignatureTemplateName = "InvalidateCCLAICLASignatureTemplate" + //InvalidateCCLASignatureTemplate ... + InvalidateCCLASignatureTemplate = ` +

    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:

    +
      + {{range .CLAManagers}} +
    • {{.Username}} {{.Email}}
    • + {{end}} +
    + ` + + //InvalidateICLASignatureTemplateName is email template upon approval list removal for ccla use case + InvalidateICLASignatureTemplateName = "InvalidateICLASignatureTemplate" + //InvalidateICLASignatureTemplate ... + InvalidateICLASignatureTemplate = ` +

    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:

    +
      + {{range .CLAManagers}} +
    • {{.Username}} {{.Email}}
    • + {{end}} +
    + ` +) + +// 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.

    + +

    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

    +
      +
    1. Select “Corporate Contributor”.
    2. +
    3. Select your company from the organization drop down list
    4. +
    5. Click Proceed
    6. +
    +

    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

    +
      +
    1. Select “Corporate Contributor”.
    2. +
    3. Select your company from the organization drop down list
    4. +
    5. Click Proceed
    6. +
    +

    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) | | user | cla | | null/empty | Yes | No | +// | CCLA/ECLA (employee) | | user | cla | | | No | Yes | +// | CCLA (CLA Manager) | | company | ccla | | null/empty | Yes | No | +type SignatureDynamoDB struct { + SignatureID string `json:"signature_id"` // PK + SignatureProjectID string `json:"signature_project_id"` // the signature CLA group ID + SignatureReferenceID string `json:"signature_reference_id"` // value is user_id for icla/ecla, value is company_id for ccla + SignatureType string `json:"signature_type"` // one of: cla, ccla + SignatureACL []string `json:"signature_acl"` // [github:1234567] + SignatureApproved bool `json:"signature_approved"` // true if the signature is approved, false if revoked/invalidated + SignatureSigned bool `json:"signature_signed"` // true if the signature has been signed + SignatureReferenceType string `json:"signature_reference_type"` // one of: user, company + SignatureReferenceName string `json:"signature_reference_name"` // John Doe + SignatureReferenceNameLower string `json:"signature_reference_name_lower"` // john doe + SignatureUserCCLACompanyID string `json:"signature_user_ccla_company_id"` // set for ECLA record types, null/missing otherwise + SignatureReturnURL string `json:"signature_return_url"` // e.g https://github.com/open-telemetry/opentelemetry-go/pull/1751 + SignatureDocumentMajorVersion int `json:"signature_document_major_version"` // 2 + SignatureDocumentMinorVersion int `json:"signature_document_minor_version"` // 0 + SigTypeSignedApprovedID string `json:"sig_type_signed_approved_id"` // e.g. ecla#true#true#e908aefe-27ff-44ea-9f06-ab513f34cb1d + SignedOn string `json:"signed_on"` // 2021-03-29T22:48:10.246463+0000 + AutoCreateECLA bool `json:"auto_create_ecla"` // flag to indicate if auto-create ECLA feature is enabled (only applies to CCLA signature record types) + ProjectID string `json:"project_id"` + ProjectName string `json:"project_name"` + ProjectSFID string `json:"project_sfid"` + CompanyID string `json:"company_id"` + CompanyName string `json:"company_name"` + CompanySFID string `json:"company_sfid"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + UserLFUsername string `json:"user_lf_username"` + UserGitHubUsername string `json:"user_github_username"` + UserGitLabUsername string `json:"user_gitlab_username"` + DateCreated string `json:"date_created"` // 2021-03-29T22:48:10.246463+0000 + DateModified string `json:"date_modified"` // 2021-08-23T22:33:03.798606+0000 + Note string `json:"note"` + Version string `json:"version"` // v1 +} + +// ActivePullRequest data model +type ActivePullRequest struct { + GitHubAuthorUsername string `json:"github_author_username"` + GitHubAuthorEmail string `json:"github_author_email"` + CLAGroupID string `json:"cla_group_id"` + RepositoryID string `json:"repository_id"` + PullRequestID string `json:"pull_request_id"` +} diff --git a/cla-backend-go/signatures/projections.go b/cla-backend-go/signatures/projections.go index e846ae7f1..20a124a54 100644 --- a/cla-backend-go/signatures/projections.go +++ b/cla-backend-go/signatures/projections.go @@ -24,11 +24,14 @@ func buildProjection() expression.ProjectionBuilder { expression.Name("signature_signed"), // T/F expression.Name("signature_type"), // ccla or cla expression.Name("signature_user_ccla_company_id"), // reference to the company - expression.Name("email_whitelist"), - expression.Name("domain_whitelist"), - expression.Name("github_whitelist"), - expression.Name("github_org_whitelist"), - expression.Name("user_github_username"), + expression.Name(SignatureEmailApprovalListColumn), + expression.Name(SignatureDomainApprovalListColumn), + expression.Name(SignatureGitHubUsernameApprovalListColumn), + expression.Name(SignatureGitHubOrgApprovalListColumn), + expression.Name(SignatureGitlabUsernameApprovalListColumn), // added for GitLab support + expression.Name(SignatureGitlabOrgApprovalListColumn), // added for GitLab support + expression.Name(SignatureUserGitHubUsername), + expression.Name(SignatureUserGitlabUsername), expression.Name("user_lf_username"), expression.Name("user_name"), expression.Name("user_email"), @@ -36,6 +39,15 @@ func buildProjection() expression.ProjectionBuilder { expression.Name("signatory_name"), expression.Name("user_docusign_date_signed"), expression.Name("user_docusign_name"), + expression.Name("auto_create_ecla"), + ) +} + +// buildCountProject is a helper function to build a simple count projection for the total count query +func buildCountProjection() expression.ProjectionBuilder { + // These are the columns we want returned - we only care about the signature_id + return expression.NamesList( + expression.Name("signature_id"), ) } @@ -56,3 +68,12 @@ func buildCompanyIDProjection() expression.ProjectionBuilder { expression.Name("signature_reference_id"), ) } + +// buildSignatureMetadata is a helper function to build a projection with key and value for signature metadata +func buildSignatureMetadata() expression.ProjectionBuilder { + // These are the columns we want returned + return expression.NamesList( + expression.Name("key"), + expression.Name("value"), + ) +} diff --git a/cla-backend-go/signatures/repository.go b/cla-backend-go/signatures/repository.go index dd69e310c..48db47aa4 100644 --- a/cla-backend-go/signatures/repository.go +++ b/cla-backend-go/signatures/repository.go @@ -5,13 +5,19 @@ package signatures import ( "context" + "encoding/base64" + "encoding/json" "errors" "fmt" "sort" + "strconv" "strings" "sync" + "time" - "github.com/go-openapi/strfmt" + "github.com/gofrs/uuid" + + "github.com/communitybridge/easycla/cla-backend-go/config" "github.com/sirupsen/logrus" @@ -19,13 +25,19 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" "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" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/v2/approvals" "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" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/aws/aws-sdk-go/aws" @@ -46,41 +58,59 @@ const ( SignatureReferenceIndex = "reference-signature-index" SignatureReferenceSearchIndex = "reference-signature-search-index" - HugePageSize = 10000 + HugePageSize = 10000 + DefaultPageSize = 100 + BigPageSize = 500 ) -// SignatureRepository interface defines the functions for the github whitelist service +// SignatureRepository interface defines the functions for the the signature repository type SignatureRepository interface { - GetGithubOrganizationsFromWhitelist(ctx context.Context, signatureID string) ([]models.GithubOrg, error) - AddGithubOrganizationToWhitelist(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error) - DeleteGithubOrganizationFromWhitelist(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error) - InvalidateProjectRecord(ctx context.Context, signatureID string, projectName string) error + GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string) ([]models.GithubOrg, error) + AddGithubOrganizationToApprovalList(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error) + DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error) + ValidateProjectRecord(ctx context.Context, signatureID, note string) error + InvalidateProjectRecord(ctx context.Context, signatureID, note string) error + UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error) + CreateSignature(ctx context.Context, signature *ItemSignature) error + UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error + SaveOrUpdateSignature(ctx context.Context, signature *ItemSignature) error GetSignature(ctx context.Context, signatureID string) (*models.Signature, error) - GetIndividualSignature(ctx context.Context, claGroupID, userID string) (*models.Signature, error) - GetCorporateSignature(ctx context.Context, claGroupID, companyID string) (*models.Signature, error) + GetItemSignature(ctx context.Context, signatureID string) (*ItemSignature, error) + GetActivePullRequestMetadata(ctx context.Context, gitHubAuthorUsername, gitHubAuthorEmail string) (*ActivePullRequest, error) + GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) + GetIndividualSignatures(ctx context.Context, claGroupID, userID string, approved, signed *bool) ([]*models.Signature, error) + GetCorporateSignature(ctx context.Context, claGroupID, companyID string, approved, signed *bool) (*models.Signature, error) + GetCorporateSignatures(ctx context.Context, claGroupID, companyID string, approved, signed *bool) ([]*models.Signature, error) + GetCCLASignatures(ctx context.Context, signed, approved *bool) ([]*ItemSignature, error) GetSignatureACL(ctx context.Context, signatureID string) ([]string, error) - GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams, pageSize int64) (*models.Signatures, error) - GetProjectCompanySignature(ctx context.Context, companyID, projectID string, signed, approved *bool, nextKey *string, pageSize *int64) (*models.Signature, error) - GetProjectCompanySignatures(ctx context.Context, companyID, projectID string, signed, approved *bool, nextKey *string, sortOrder *string, pageSize *int64) (*models.Signatures, error) - GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, pageSize int64) (*models.Signatures, error) + GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams) (*models.Signatures, error) + CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) + GetProjectCompanySignature(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, pageSize *int64) (*models.Signature, error) + GetProjectCompanySignatures(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, sortOrder *string, pageSize *int64) (*models.Signatures, error) + GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *ApprovalCriteria) (*models.Signatures, error) + GetProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User, wg *sync.WaitGroup, resultChannel chan<- *EmployeeModel, errorChannel chan<- error) + CreateProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User) error GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams, pageSize int64, loadACL bool) (*models.Signatures, error) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]SignatureCompanyID, error) - GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64) (*models.Signatures, error) + GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64, projectID *string) (*models.Signatures, error) ProjectSignatures(ctx context.Context, projectID string) (*models.Signatures, error) - UpdateApprovalList(ctx context.Context, projectID, companyID string, params *models.ApprovalList) (*models.Signature, error) - + UpdateApprovalList(ctx context.Context, claManager *models.User, claGroupModel *models.ClaGroup, companyID string, params *models.ApprovalList, eventArgs *events.LogEventArgs) (*models.Signature, error) AddCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) RemoveCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) - - removeColumn(ctx context.Context, signatureID, columnName string) (*models.Signature, error) - AddSigTypeSignedApprovedID(ctx context.Context, signatureID string, val string) error AddUsersDetails(ctx context.Context, signatureID string, userID string) error AddSignedOn(ctx context.Context, signatureID string) error + GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) + GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey *string, searchTerm *string) (*models.CorporateContributorList, error) + EclaAutoCreate(ctx context.Context, signatureID string, autoCreateECLA bool) error + ActivateSignature(ctx context.Context, signatureID string) error + GetICLAByDate(ctx context.Context, startDate string) ([]ItemSignature, error) +} - GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string) (*models.IclaSignatures, error) - GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, searchTerm *string) (*models.CorporateContributorList, error) +type iclaSignatureWithDetails struct { + IclaSignature *models.IclaSignature + SignatureReferenceID string } // repository data model @@ -89,24 +119,278 @@ type repository struct { dynamoDBClient *dynamodb.DynamoDB companyRepo company.IRepository usersRepo users.UserRepository + eventsService events.Service + repositoriesRepo repositories.RepositoryInterface + ghOrgRepo github_organizations.RepositoryInterface + gerritService gerrits.Service signatureTableName string + approvalRepo approvals.IRepository } -// NewRepository creates a new instance of the whitelist service -func NewRepository(awsSession *session.Session, stage string, companyRepo company.IRepository, usersRepo users.UserRepository) SignatureRepository { +// NewRepository creates a new instance of the signature repository service +func NewRepository(awsSession *session.Session, stage string, companyRepo company.IRepository, usersRepo users.UserRepository, eventsService events.Service, repositoriesRepo repositories.RepositoryInterface, ghOrgRepo github_organizations.RepositoryInterface, gerritService gerrits.Service, approvalRepo approvals.IRepository) SignatureRepository { return repository{ stage: stage, dynamoDBClient: dynamodb.New(awsSession), companyRepo: companyRepo, usersRepo: usersRepo, + eventsService: eventsService, + repositoriesRepo: repositoriesRepo, + ghOrgRepo: ghOrgRepo, + gerritService: gerritService, signatureTableName: fmt.Sprintf("cla-%s-signatures", stage), + approvalRepo: approvalRepo, + } +} + +// CreateIndividualSignature creates a new individual signature +func (repo repository) CreateSignature(ctx context.Context, signature *ItemSignature) error { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.CreateIndividualSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + av, err := dynamodbattribute.MarshalMap(signature) + if err != nil { + log.WithFields(f).Warnf("error marshalling signature, error: %v", err) + return err + } + + // Add the signature to the database + _, err = repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(repo.signatureTableName), + }) + + if err != nil { + log.WithFields(f).Warnf("error adding signature to database, error: %v", err) + return err + } + + log.WithFields(f).Debugf("successfully added signature to database") + + return nil + +} + +// GetItemSignature returns the signature for the specified signature id +func (repo repository) GetItemSignature(ctx context.Context, signatureID string) (*ItemSignature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetItemSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + } + + // This is the key we want to match + condition := expression.Key("signature_id").Equal(expression.Value(signatureID)) + + // Use the builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for signature ID query, signatureID: %s, error: %v", signatureID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + TableName: aws.String(repo.signatureTableName), + } + + // Make the DynamoDB Query API call + results, queryErr := repo.dynamoDBClient.Query(queryInput) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving signature ID: %s, error: %v", signatureID, queryErr) + return nil, queryErr + } + + // No match, didn't find it + if *results.Count == 0 { + return nil, nil + } + + var signature ItemSignature + err = dynamodbattribute.UnmarshalMap(results.Items[0], &signature) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling signature for ID: %s, error: %v", signatureID, err) + return nil, err + } + + return &signature, nil +} + +// SaveOrUpdateSignature either creates or updates the signature record +func (repo repository) SaveOrUpdateSignature(ctx context.Context, signature *ItemSignature) error { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.SaveOrUpdateSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + av, err := dynamodbattribute.MarshalMap(signature) + + if err != nil { + log.WithFields(f).Warnf("error marshalling signature, error: %v", err) + return err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(repo.signatureTableName), + } + + _, err = repo.dynamoDBClient.PutItem(input) + if err != nil { + log.WithFields(f).Warnf("error adding signature to database, error: %v", err) + return err + } + + log.WithFields(f).Debugf("successfully added/updated signature to database") + + return nil +} + +// GetCCCLASignatures returns a list of CCLA signatures +func (repo repository) GetCCLASignatures(ctx context.Context, signed, approved *bool) ([]*ItemSignature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetCCLASignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signed": signed, + "approved": approved, + } + + var filter expression.ConditionBuilder + pageSize := 1000 + + filter = expression.Name("signature_type").Equal(expression.Value("ccla")) + if signed != nil { + filter = filter.And(expression.Name("signature_signed").Equal(expression.Value(signed))) + } + if approved != nil { + filter = filter.And(expression.Name("signature_approved").Equal(expression.Value(approved))) + } + + // Use the expression builder to build the expression + expr, err := expression.NewBuilder().WithFilter(filter).Build() + + if err != nil { + log.WithFields(f).Warnf("error building expression for CCLA signatures query, error: %v", err) + return nil, err + } + + // Make the DynamoDB Query API call + input := &dynamodb.ScanInput{ + TableName: aws.String(repo.signatureTableName), + FilterExpression: expr.Filter(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + Limit: aws.Int64(int64(pageSize)), + } + + var signatures []*ItemSignature + var lastEvaluatedKey map[string]*dynamodb.AttributeValue + + for { + results, queryErr := repo.dynamoDBClient.Scan(input) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving CCLA signatures, error: %v", queryErr) + return nil, queryErr + } + + var items []*ItemSignature + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &items) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling CCLA signatures from database, error: %v", err) + return nil, err + } + + signatures = append(signatures, items...) + + // If the result set is truncated, we'll need to issue another query to fetch the next page + if results.LastEvaluatedKey == nil { + break + } + + lastEvaluatedKey = results.LastEvaluatedKey + input.ExclusiveStartKey = lastEvaluatedKey + } + + return signatures, nil + +} + +// UpdateSignature updates an existing signature +func (repo repository) UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.UpdateSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + } + + if len(updates) == 0 { + log.WithFields(f).Warnf("no updates provided") + return errors.New("no updates provided") + } + + var updateExpression strings.Builder + updateExpression.WriteString("SET ") + attributeValues := make(map[string]*dynamodb.AttributeValue) + expressionAttributeNames := make(map[string]*string) + + count := 1 + for attr, val := range updates { + attrPlaceholder := fmt.Sprintf("#A%d", count) + valPlaceholder := fmt.Sprintf(":v%d", count) + + if count > 1 && count <= len(updates) { + updateExpression.WriteString(", ") + } + updateExpression.WriteString(fmt.Sprintf("%s = %s", attrPlaceholder, valPlaceholder)) + + expressionAttributeNames[attrPlaceholder] = aws.String(attr) + av, err := dynamodbattribute.Marshal(val) + if err != nil { + return err + } + attributeValues[valPlaceholder] = av + + count++ + } + + log.WithFields(f).Debugf("updating signature using expression: %s", updateExpression.String()) + log.WithFields(f).Debugf("expression attribute names : %+v", expressionAttributeNames) + + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: attributeValues, + TableName: aws.String(repo.signatureTableName), + Key: map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(signatureID), + }, + }, + UpdateExpression: aws.String(updateExpression.String()), + ExpressionAttributeNames: expressionAttributeNames, + ReturnValues: aws.String("UPDATED_NEW"), + } + + // perform the update + _, err := repo.dynamoDBClient.UpdateItem(input) + if err != nil { + log.WithFields(f).Warnf("error updating signature, error: %v", err) + return err } + + log.WithFields(f).Debugf("successfully updated signature") + + return nil + } -// GetGithubOrganizationsFromWhitelist returns a list of GH organizations stored in the whitelist -func (repo repository) GetGithubOrganizationsFromWhitelist(ctx context.Context, signatureID string) ([]models.GithubOrg, error) { +// GetGithubOrganizationsFromApprovalList returns a list of GH organizations stored in the approval list +func (repo repository) GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string) ([]models.GithubOrg, error) { f := logrus.Fields{ - "functionName": "GetGitHubOrganizationsFromWhitelist", + "functionName": "v1.signatures.repository.GetGithubOrganizationsFromApprovalList", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, } @@ -121,7 +405,7 @@ func (repo repository) GetGithubOrganizationsFromWhitelist(ctx context.Context, }) if err != nil { - log.WithFields(f).Warnf("Error retrieving GH organization whitelist for signatureID: %s, error: %v", signatureID, err) + log.WithFields(f).Warnf("Error retrieving GH organization approval list for signatureID: %s, error: %v", signatureID, err) return nil, err } @@ -147,16 +431,16 @@ func (repo repository) GetGithubOrganizationsFromWhitelist(ctx context.Context, return orgs, nil } -// AddGithubOrganizationToWhitelist adds the specified GH organization to the whitelist -func (repo repository) AddGithubOrganizationToWhitelist(ctx context.Context, signatureID, GitHubOrganizationID string) ([]models.GithubOrg, error) { +// AddGithubOrganizationToApprovalList adds the specified GH organization to the approval list +func (repo repository) AddGithubOrganizationToApprovalList(ctx context.Context, signatureID, GitHubOrganizationID string) ([]models.GithubOrg, error) { f := logrus.Fields{ - "functionName": "AddGitHubOrganizationToWhitelist", + "functionName": "v1.signatures.repository.AddGithubOrganizationToApprovalList", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, "GitHubOrganizationID": GitHubOrganizationID, } // get item from dynamoDB table - log.WithFields(f).Debugf("querying database for GitHub organization whitelist using signatureID: %s", signatureID) + log.WithFields(f).Debugf("querying database for GitHub organization approval list using signatureID: %s", signatureID) result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ TableName: aws.String(repo.signatureTableName), @@ -168,7 +452,7 @@ func (repo repository) AddGithubOrganizationToWhitelist(ctx context.Context, sig }) if err != nil { - log.WithFields(f).Warnf("Error retrieving GitHub organization whitelist for signatureID: %s and GH Org: %s, error: %v", + log.WithFields(f).Warnf("Error retrieving GitHub organization approval list for signatureID: %s and GH Org: %s, error: %v", signatureID, GitHubOrganizationID, err) return nil, err } @@ -230,7 +514,7 @@ func (repo repository) AddGithubOrganizationToWhitelist(ctx context.Context, sig updatedItemFromMap, ok := updatedValues.Attributes["github_org_whitelist"] if !ok { - msg := fmt.Sprintf("unable to fetch updated whitelist organization values for "+ + msg := fmt.Sprintf("unable to fetch updated github organization approval list values for "+ "organization id: %s for signature: %s - list is empty - returning empty list", GitHubOrganizationID, signatureID) log.WithFields(f).Debugf(msg) @@ -240,10 +524,10 @@ func (repo repository) AddGithubOrganizationToWhitelist(ctx context.Context, sig return buildResponse(updatedItemFromMap.L), nil } -// DeleteGithubOrganizationFromWhitelist removes the specified GH organization from the whitelist -func (repo repository) DeleteGithubOrganizationFromWhitelist(ctx context.Context, signatureID, GitHubOrganizationID string) ([]models.GithubOrg, error) { +// DeleteGithubOrganizationFromApprovalList removes the specified GH organization from the approval list +func (repo repository) DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID, GitHubOrganizationID string) ([]models.GithubOrg, error) { f := logrus.Fields{ - "functionName": "DeleteGitHubOrganizationFromWhitelist", + "functionName": "v1.signatures.repository.DeleteGithubOrganizationFromApprovalList", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, "GitHubOrganizationID": GitHubOrganizationID, @@ -259,14 +543,14 @@ func (repo repository) DeleteGithubOrganizationFromWhitelist(ctx context.Context }) if err != nil { - log.WithFields(f).Warnf("error retrieving GH organization whitelist for signatureID: %s and GH Org: %s, error: %v", + log.WithFields(f).Warnf("error retrieving GH organization approval list for signatureID: %s and GH Org: %s, error: %v", signatureID, GitHubOrganizationID, err) return nil, err } itemFromMap, ok := result.Item["github_org_whitelist"] if !ok { - log.WithFields(f).Warnf("unable to remove whitelist organization: %s for signature: %s - list is empty", + log.WithFields(f).Warnf("unable to remove github organization approval list entry: %s for signature: %s - list is empty", GitHubOrganizationID, signatureID) return nil, errors.New("no github_org_whitelist column") } @@ -286,7 +570,7 @@ func (repo repository) DeleteGithubOrganizationFromWhitelist(ctx context.Context // ValidationException: ExpressionAttributeValues contains invalid value: Supplied AttributeValue // is empty, must contain exactly one of the supported data types for the key) - log.WithFields(f).Debugf("clearing out github org whitelist for organization: %s for signature: %s - list is empty", + log.WithFields(f).Debugf("clearing out github org approval list for organization: %s for signature: %s - list is empty", GitHubOrganizationID, signatureID) nullFlag := true @@ -311,7 +595,7 @@ func (repo repository) DeleteGithubOrganizationFromWhitelist(ctx context.Context _, err = repo.dynamoDBClient.UpdateItem(input) if err != nil { - log.WithFields(f).Warnf("error updating github org whitelist to NULL value, error: %v", err) + log.WithFields(f).Warnf("error updating github org approva list to NULL value, error: %v", err) return nil, err } @@ -344,13 +628,13 @@ func (repo repository) DeleteGithubOrganizationFromWhitelist(ctx context.Context updatedValues, err := repo.dynamoDBClient.UpdateItem(input) if err != nil { - log.WithFields(f).Warnf("Error updating github org whitelist, error: %v", err) + log.WithFields(f).Warnf("Error updating github org approva list, error: %v", err) return nil, err } updatedItemFromMap, ok := updatedValues.Attributes["github_org_whitelist"] if !ok { - msg := fmt.Sprintf("unable to fetch updated whitelist organization values for "+ + msg := fmt.Sprintf("unable to fetch updated approva list organization values for "+ "organization id: %s for signature: %s - list is empty - returning empty list", GitHubOrganizationID, signatureID) log.WithFields(f).Debugf(msg) @@ -364,7 +648,7 @@ func (repo repository) DeleteGithubOrganizationFromWhitelist(ctx context.Context // GetSignature returns the signature for the specified signature id func (repo repository) GetSignature(ctx context.Context, signatureID string) (*models.Signature, error) { f := logrus.Fields{ - "functionName": "GetSignature", + "functionName": "v1.signatures.repository.GetSignature", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, } @@ -415,10 +699,72 @@ func (repo repository) GetSignature(ctx context.Context, signatureID string) (*m return signatureList[0], nil } +// CreateOrUpdateSignature either creates or updates the signature record +func (repo repository) UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.UpdateEnvelopeDetails", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + "envelopeID": envelopeID, + } + + log.WithFields(f).Debugf("setting envelope details....") + + updateExpression := "SET signature_envelope_id = :envelopeId " + expressionAttributeValues := map[string]*dynamodb.AttributeValue{ + ":envelopeId": { + S: aws.String(envelopeID), + }, + } + + if signURL != nil { + updateExpression += ",signature_sign_url = :signUrl " + expressionAttributeValues[":signUrl"] = &dynamodb.AttributeValue{ + S: aws.String(*signURL), + } + } + + // Create the update input + input := &dynamodb.UpdateItemInput{ + TableName: aws.String(repo.signatureTableName), + Key: map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(signatureID), + }, + }, + UpdateExpression: aws.String(updateExpression), + ExpressionAttributeValues: expressionAttributeValues, + ReturnValues: aws.String("ALL_NEW"), + } + + // Update the record in the DynamoDB table + result, err := repo.dynamoDBClient.UpdateItem(input) + if err != nil { + log.WithFields(f).Errorf("Error updating signature record: %v", err) + return nil, err + } + + // Update the record in the DynamoDB table + var updatedItem ItemSignature + + if err := dynamodbattribute.UnmarshalMap(result.Attributes, &updatedItem); err != nil { + log.WithFields(f).Errorf("Error unmarshalling updated item: %v", err) + return nil, err + } + + log.WithFields(f).Debugf("updated signature record for: %s", signatureID) + return &models.Signature{ + SignatureID: updatedItem.SignatureID, + SignatureSignURL: updatedItem.SignatureSignURL, + ProjectID: updatedItem.SignatureProjectID, + SignatureReferenceID: updatedItem.SignatureReferenceID, + }, nil +} + // GetIndividualSignature returns the signature record for the specified CLA Group and User -func (repo repository) GetIndividualSignature(ctx context.Context, claGroupID, userID string) (*models.Signature, error) { +func (repo repository) GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) { f := logrus.Fields{ - "functionName": "GetIndividualSignature", + "functionName": "v1.signatures.repository.GetIndividualSignature", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "tableName": repo.signatureTableName, "claGroupID": claGroupID, @@ -429,14 +775,36 @@ func (repo repository) GetIndividualSignature(ctx context.Context, claGroupID, u "signatureSigned": "true", } + log.WithFields(f).Debug("querying signature for icla records ...") + + var filterAdded bool // These are the keys we want to match for an ICLA Signature with a given CLA Group and User ID condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)). And(expression.Key("signature_reference_id").Equal(expression.Value(userID))) - filter := expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)). - And(expression.Name("signature_reference_type").Equal(expression.Value("user"))). - And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_user_ccla_company_id").AttributeNotExists()) + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + + if approved != nil { + filterAdded = true + searchTermExpression := expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(approved))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + if signed != nil { + filterAdded = true + log.WithFields(f).Debugf("adding filter signature_signed: %t", aws.BoolValue(signed)) + searchTermExpression := expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(signed))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if approved == nil && signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + // log.WithFields(f).Debug("adding filter signature_approved: true and signature_signed: true") + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } builder := expression.NewBuilder(). WithKeyCondition(condition). @@ -468,7 +836,6 @@ func (repo repository) GetIndividualSignature(ctx context.Context, claGroupID, u // Loop until we have all the records for ok := true; ok; ok = lastEvaluatedKey != "" { // Make the DynamoDB Query API call - //log.WithFields(f).Debugf("Running signature project query using queryInput: %+v", queryInput) results, errQuery := repo.dynamoDBClient.Query(queryInput) //log.WithFields(f).Debugf("Ran signature project query, results: %+v, error: %+v", results, errQuery) if errQuery != nil { @@ -506,31 +873,53 @@ func (repo repository) GetIndividualSignature(ctx context.Context, claGroupID, u log.WithFields(f).Warnf("found multiple matching ICLA signatures - found %d total", len(sigs)) } - return sigs[0], nil + return sigs[0], nil // nolint G602: Potentially accessing slice out of bounds (gosec) } -// GetCorporateSignature returns the signature record for the specified CLA Group and Company ID -func (repo repository) GetCorporateSignature(ctx context.Context, claGroupID, companyID string) (*models.Signature, error) { +// GetIndividualSignature returns the signature record for the specified CLA Group and User +func (repo repository) GetIndividualSignatures(ctx context.Context, claGroupID, userID string, approved, signed *bool) ([]*models.Signature, error) { f := logrus.Fields{ - "functionName": "GetCorporateSignature", + "functionName": "v1.signatures.repository.GetIndividualSignature", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "tableName": repo.signatureTableName, "claGroupID": claGroupID, - "companyID": companyID, - "signatureType": "ccla", - "signatureReferenceType": "company", + "userID": userID, + "signatureType": utils.SignatureTypeCLA, + "signatureReferenceType": utils.SignatureReferenceTypeUser, "signatureApproved": "true", "signatureSigned": "true", } + log.WithFields(f).Debug("querying signature for icla records ...") + + var filterAdded bool // These are the keys we want to match for an ICLA Signature with a given CLA Group and User ID condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)). - And(expression.Key("signature_reference_id").Equal(expression.Value(companyID))) - filter := expression.Name("signature_type").Equal(expression.Value("ccla")). - And(expression.Name("signature_reference_type").Equal(expression.Value("company"))). - And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_user_ccla_company_id").AttributeNotExists()) + And(expression.Key("signature_reference_id").Equal(expression.Value(userID))) + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + + if approved != nil { + filterAdded = true + searchTermExpression := expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(approved))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + if signed != nil { + filterAdded = true + log.WithFields(f).Debugf("adding filter signature_signed: %t", aws.BoolValue(signed)) + searchTermExpression := expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(signed))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if approved == nil && signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + // log.WithFields(f).Debug("adding filter signature_approved: true and signature_signed: true") + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } builder := expression.NewBuilder(). WithKeyCondition(condition). @@ -540,7 +929,7 @@ func (repo repository) GetCorporateSignature(ctx context.Context, claGroupID, co // Use the nice builder to create the expression expr, err := builder.Build() if err != nil { - log.WithFields(f).Warnf("error building expression for project CCLA signature query, error: %v", err) + log.WithFields(f).Warnf("error building expression for project ICLA signature query, error: %v", err) return nil, err } @@ -563,8 +952,9 @@ func (repo repository) GetCorporateSignature(ctx context.Context, claGroupID, co for ok := true; ok; ok = lastEvaluatedKey != "" { // Make the DynamoDB Query API call results, errQuery := repo.dynamoDBClient.Query(queryInput) + //log.WithFields(f).Debugf("Ran signature project query, results: %+v, error: %+v", results, errQuery) if errQuery != nil { - log.WithFields(f).Warnf("error retrieving project CCLA signature, error: %v", errQuery) + log.WithFields(f).Warnf("error retrieving project ICLA signature ID, error: %v", errQuery) return nil, errQuery } @@ -598,24 +988,325 @@ func (repo repository) GetCorporateSignature(ctx context.Context, claGroupID, co log.WithFields(f).Warnf("found multiple matching ICLA signatures - found %d total", len(sigs)) } - return sigs[0], nil + return sigs, nil } -// GetSignatureACL returns the signature ACL for the specified signature id -func (repo repository) GetSignatureACL(ctx context.Context, signatureID string) ([]string, error) { +// GetCorporateSignature returns the signature record for the specified CLA Group and Company ID +func (repo repository) GetCorporateSignature(ctx context.Context, claGroupID, companyID string, approved, signed *bool) (*models.Signature, error) { f := logrus.Fields{ - "functionName": "GetSignatureACL", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "signatureID": signatureID, - } - // Use the nice builder to create the expression - expr, err := expression.NewBuilder(). - WithProjection(buildSignatureACLProjection()). - Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for signature ID query, signatureID: %s, error: %v", - signatureID, err) - return nil, err + "functionName": "v1.signatures.repository.GetCorporateSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "tableName": repo.signatureTableName, + "claGroupID": claGroupID, + "companyID": companyID, + "signatureType": "ccla", + "signatureReferenceType": "company", + "signatureApproved": utils.BoolValue(approved), + "signatureSigned": utils.BoolValue(signed), + } + + var filterAdded bool + // These are the keys we want to match for an CCLA Signature with a given CLA Group and Company ID + condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)). + And(expression.Key("signature_reference_id").Equal(expression.Value(companyID))) + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + + if approved != nil { + filterAdded = true + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(approved))), &filterAdded) + } + if signed != nil { + filterAdded = true + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(signed))), &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if approved == nil && signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + //log.WithFields(f).Debug("adding filter signature_approved: true and signature_signed: true") + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } + + builder := expression.NewBuilder(). + WithKeyCondition(condition). + WithFilter(filter). + WithProjection(buildProjection()) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project CCLA signature query, error: %v", err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + Limit: aws.Int64(100), // The maximum number of items to evaluate (not necessarily the number of matching items) + IndexName: aws.String(SignatureProjectReferenceIndex), // Name of a secondary index to scan + } + + sigs := make([]*models.Signature, 0) + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + // Make the DynamoDB Query API call + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving project CCLA signature, error: %v", errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + //log.WithFields(f).Debug("Building response models...") + signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, claGroupID, LoadACLDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signatures, error: %v", + modelErr) + return nil, modelErr + } + + // Add to the signatures response model to the list + sigs = append(sigs, signatureList...) + + //log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey) + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" + } + } + + // Didn't find a matching record + if len(sigs) == 0 { + return nil, nil + } + + if len(sigs) > 1 { + log.WithFields(f).Warnf("found multiple matching ICLA signatures - found %d total", len(sigs)) + } + + return sigs[0], nil // nolint G602: Potentially accessing slice out of bounds (gosec) +} + +// GetCorporateSignatures returns the list signature record for the specified CLA Group and Company ID +func (repo repository) GetCorporateSignatures(ctx context.Context, claGroupID, companyID string, approved, signed *bool) ([]*models.Signature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetCorporateSignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "tableName": repo.signatureTableName, + "claGroupID": claGroupID, + "companyID": companyID, + "signatureType": "ccla", + "signatureReferenceType": "company", + "signatureApproved": utils.BoolValue(approved), + "signatureSigned": utils.BoolValue(signed), + } + + var filterAdded bool + // These are the keys we want to match for an CCLA Signature with a given CLA Group and Company ID + condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)). + And(expression.Key("signature_reference_id").Equal(expression.Value(companyID))) + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + + if approved != nil { + filterAdded = true + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(approved))), &filterAdded) + } + if signed != nil { + filterAdded = true + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(signed))), &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if approved == nil && signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + //log.WithFields(f).Debug("adding filter signature_approved: true and signature_signed: true") + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } + + builder := expression.NewBuilder(). + WithKeyCondition(condition). + WithFilter(filter). + WithProjection(buildProjection()) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project CCLA signature query, error: %v", err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + Limit: aws.Int64(100), // The maximum number of items to evaluate (not necessarily the number of matching items) + IndexName: aws.String(SignatureProjectReferenceIndex), // Name of a secondary index to scan + } + + sigs := make([]*models.Signature, 0) + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + // Make the DynamoDB Query API call + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving project CCLA signature, error: %v", errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + //log.WithFields(f).Debug("Building response models...") + signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, claGroupID, LoadACLDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signatures, error: %v", + modelErr) + return nil, modelErr + } + + // Add to the signatures response model to the list + sigs = append(sigs, signatureList...) + + //log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey) + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" + } + } + + // Didn't find a matching record + if len(sigs) == 0 { + return nil, nil + } + + if len(sigs) > 1 { + log.WithFields(f).Warnf("found multiple matching ICLA signatures - found %d total", len(sigs)) + } + + return sigs, nil +} + +// GetActivePullRequestMetadata returns the pull request metadata for the given user ID +func (repo repository) GetActivePullRequestMetadata(ctx context.Context, gitHubAuthorUsername, gitHubAuthorEmail string) (*ActivePullRequest, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetActivePullRequestMetadata", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitHubAuthorUsername": gitHubAuthorUsername, + "gitHubAuthorEmail": gitHubAuthorEmail, + } + + if gitHubAuthorUsername == "" && gitHubAuthorEmail == "" { + return nil, nil + } + + expr, err := expression.NewBuilder().WithProjection(buildSignatureMetadata()).Build() + if err != nil { + log.WithFields(f).WithError(err).Warn("error building expression for user ID query") + return nil, err + } + + // Try to lookup based on the following keys - could be indexed by either or both (depends if user shared their + // email and went through the GitHub authorization flow) + var keys []string + if gitHubAuthorUsername != "" { + keys = append(keys, fmt.Sprintf("active_pr:u:%s", gitHubAuthorUsername)) + } + if gitHubAuthorEmail != "" { + keys = append(keys, fmt.Sprintf("active_pr:e:%s", gitHubAuthorEmail)) + } + + var activeSignature ActivePullRequest + for _, key := range keys { + itemInput := &dynamodb.GetItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String(key)}, + }, + ExpressionAttributeNames: expr.Names(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(fmt.Sprintf("cla-%s-store", repo.stage)), + } + + // Make the DynamoDb Query API call + // log.WithFields(f).Debugf("loading active signature using key: %s", key) + result, queryErr := repo.dynamoDBClient.GetItem(itemInput) + if queryErr != nil { + if queryErr.Error() == dynamodb.ErrCodeResourceNotFoundException { + continue + } + log.WithFields(f).WithError(queryErr).Warnf("error retrieving active signature metadata using key: %s", key) + return nil, queryErr + } + + if result == nil || result.Item == nil || result.Item["value"] == nil || result.Item["value"].S == nil { + log.WithFields(f).Debugf("query result is empty for key: %s", key) + continue + } + if result.Item["value"] == nil || result.Item["value"].S == nil { + log.WithFields(f).Debugf("query result value is empty for key: %s", key) + continue + } + + // Clean up the JSON string + strValue := utils.StringValue(result.Item["value"].S) + // log.WithFields(f).Debugf("decoding value: %s", strValue) + if strings.HasSuffix(strValue, "\"") { + // Trim the leading and trailing quotes from the JSON record + strValue = strValue[1 : len(strValue)-1] + } + // Unescape the JSON string + strValue = strings.Replace(strValue, "\\\"", "\"", -1) + // log.WithFields(f).Debugf("decoding value: %s", strValue) + + jsonUnMarshallErr := json.Unmarshal([]byte(strValue), &activeSignature) + if jsonUnMarshallErr != nil { + log.WithFields(f).WithError(jsonUnMarshallErr).Warn("unable to convert model for active signature ") + return nil, jsonUnMarshallErr + } + + return &activeSignature, nil + } + + return nil, nil +} + +// GetSignatureACL returns the signature ACL for the specified signature id +func (repo repository) GetSignatureACL(ctx context.Context, signatureID string) ([]string, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetSignatureACL", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + } + // Use the nice builder to create the expression + expr, err := expression.NewBuilder(). + WithProjection(buildSignatureACLProjection()). + Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for signature ID query, signatureID: %s, error: %v", + signatureID, err) + return nil, err } // Assemble the query input parameters @@ -652,20 +1343,20 @@ func (repo repository) GetSignatureACL(ctx context.Context, signatureID string) return dbModel.SignatureACL, nil } -func addConditionToFilter(filter expression.ConditionBuilder, cond expression.ConditionBuilder, filterAdded *bool) expression.ConditionBuilder { - if !(*filterAdded) { - *filterAdded = true - filter = cond - } else { - filter = filter.And(cond) +// Adds the specified expression to the current filter using the And operator. This routine checks the filter added flag +// to determine if a previous filter was set. After this function executes, the filterAdded value will be set to true. +func addAndCondition(filter expression.ConditionBuilder, cond expression.ConditionBuilder, filterAdded *bool) expression.ConditionBuilder { + if *filterAdded { + return filter.And(cond) } - return filter + *filterAdded = true + return cond } // GetProjectSignatures returns a list of signatures for the specified project -func (repo repository) GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams, pageSize int64) (*models.Signatures, error) { +func (repo repository) GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams) (*models.Signatures, error) { // nolint f := logrus.Fields{ - "functionName": "GetProjectSignatures", + "functionName": "v1.signatures.repository.GetProjectSignatures", "tableName": repo.signatureTableName, utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ProjectID, @@ -678,94 +1369,850 @@ func (repo repository) GetProjectSignatures(ctx context.Context, params signatur "sortOrder": aws.StringValue(params.SortOrder), } - indexName := SignatureProjectIDIndex - if params.SortOrder != nil && *params.SortOrder != "" { - indexName = SignatureProjectDateIDIndex - } + // Always sort by date + indexName := SignatureProjectDateIDIndex - realPageSize := int64(100) + realPageSize := int64(1000) if params.PageSize != nil && *params.PageSize > 0 { realPageSize = *params.PageSize } // This is the key we want to match condition := expression.Key("signature_project_id").Equal(expression.Value(params.ProjectID)) + builder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()) - builder := expression.NewBuilder().WithProjection(buildProjection()) var filter expression.ConditionBuilder - var filterAdded bool + var filterAdded = false + + if params.ClaType != nil || params.SignatureType != nil { + switch getCLATypeFromParams(params) { + case utils.ClaTypeICLA: + log.WithFields(f).Debugf("adding ICLA filters: signature_type: %s, signature_reference_type: %s, signature_user_ccla_company_id: not exists", utils.SignatureTypeCLA, utils.SignatureReferenceTypeUser) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + case utils.ClaTypeECLA: + log.WithFields(f).Debugf("adding ECLA filters: signature_type: %s, signature_reference_type: %s, signature_user_ccla_company_id: exists", utils.SignatureTypeCLA, utils.SignatureReferenceTypeUser) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeExists(), &filterAdded) + case utils.ClaTypeCCLA: + log.WithFields(f).Debugf("adding CCLA filters: signature_type: %s, signature_reference_type: %s, signature_user_ccla_company_id: not exists", utils.SignatureTypeCCLA, utils.SignatureReferenceTypeCompany) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + } + } - if params.ClaType != nil { - filterAdded = true - if strings.ToLower(*params.ClaType) == utils.ClaTypeICLA { - filter = expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)). - And(expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser))). - And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_user_ccla_company_id").AttributeNotExists()) + if params.SearchTerm != nil && utils.StringValue(params.SearchTerm) != "" { + //if *params.FullMatch { + // indexName = SignatureReferenceSearchIndex + // log.WithFields(f).Debugf("adding filter signature_reference_name_lower: %s", strings.ToLower(utils.StringValue(params.SearchTerm))) + // condition = condition.And(expression.Key("signature_reference_name_lower").Equal(expression.Value(strings.ToLower(utils.StringValue(params.SearchTerm))))) + //} // else { + log.WithFields(f).Debugf("adding filters signature_reference_name_lower: %s or user_email: %s", strings.ToLower(utils.StringValue(params.SearchTerm)), strings.ToLower(utils.StringValue(params.SearchTerm))) + searchTermExpression := expression.Name("signature_reference_name_lower").Contains(strings.ToLower(utils.StringValue(params.SearchTerm))). + Or(expression.Name("user_email").Contains(strings.ToLower(utils.StringValue(params.SearchTerm)))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + //} + } - } else if strings.ToLower(*params.ClaType) == utils.ClaTypeECLA { - filter = expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)). - And(expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser))). - And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_user_ccla_company_id").AttributeExists()) - } else if strings.ToLower(*params.ClaType) == utils.ClaTypeCCLA { - filter = expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)). - And(expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany))). - And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_user_ccla_company_id").AttributeNotExists()) - } - } else { - if params.SearchField != nil { - searchFieldExpression := expression.Name("signature_reference_type").Equal(expression.Value(params.SearchField)) - filter = addConditionToFilter(filter, searchFieldExpression, &filterAdded) - } + if params.Approved != nil { + log.WithFields(f).Debugf("adding signature_approved: %t filter", aws.BoolValue(params.Approved)) + searchTermExpression := expression.Name("signature_approved").Equal(expression.Value(params.Approved)) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } - if params.SignatureType != nil { - if params.SearchTerm != nil && (params.FullMatch != nil && !*params.FullMatch) { - indexName = SignatureProjectIDTypeIndex - condition = condition.And(expression.Key("signature_type").Equal(expression.Value(strings.ToLower(*params.SignatureType)))) - } else { - signatureTypeExpression := expression.Name("signature_type").Equal(expression.Value(params.SignatureType)) - filter = addConditionToFilter(filter, signatureTypeExpression, &filterAdded) - } - if *params.SignatureType == "ccla" { - signatureReferenceIDExpression := expression.Name("signature_reference_id").AttributeExists() - signatureUserCclaCompanyIDExpression := expression.Name("signature_user_ccla_company_id").AttributeNotExists() - filter = addConditionToFilter(filter, signatureReferenceIDExpression, &filterAdded) - filter = addConditionToFilter(filter, signatureUserCclaCompanyIDExpression, &filterAdded) + if params.Signed != nil { + log.WithFields(f).Debugf("adding signature_signed: %t filter", aws.BoolValue(params.Signed)) + searchTermExpression := expression.Name("signature_signed").Equal(expression.Value(params.Signed)) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if params.Approved == nil && params.Signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + log.WithFields(f).Debug("adding signature_approved: true and signature_signed: true filters") + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } + + if filterAdded { + builder = builder.WithFilter(filter) + } + + // Use the builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).WithError(err).Warnf("error building expression for project signature query, projectID: %s, error: %v", params.ProjectID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + Limit: aws.Int64(realPageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) + IndexName: aws.String(indexName), // Name of a secondary index to scan + } + f["indexName"] = indexName + log.WithFields(f).Debugf("queryInput: %+v", queryInput) + + if params.NextKey != nil { + queryInput.ExclusiveStartKey, err = decodeNextKey(*params.NextKey) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem decoding next key value") + return nil, err + } + log.WithFields(f).Debugf("received a nextKey, value: %s - decoded: %+v", *params.NextKey, queryInput.ExclusiveStartKey) + } + + sigs := make([]*models.Signature, 0) + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + // Make the DynamoDB Query API call + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving project signature ID for project: %s, error: %v", + params.ProjectID, errQuery) + return nil, errQuery + } + log.WithFields(f).Debugf("returned %d results", len(results.Items)) + + // Convert the list of DB models to a list of response models + signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, params.ProjectID, LoadACLDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s, error: %v", + params.ProjectID, modelErr) + return nil, modelErr + } + + // Add to the signatures response model to the list + sigs = append(sigs, signatureList...) + + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" + } + + if int64(len(sigs)) >= realPageSize { + break + } + } + + // How many total records do we have - may not be up-to-date as this value is updated only periodically + describeTableInput := &dynamodb.DescribeTableInput{ + TableName: &repo.signatureTableName, + } + describeTableResult, err := repo.dynamoDBClient.DescribeTable(describeTableInput) + if err != nil { + log.WithFields(f).Warnf("error retrieving total record count for project: %s, error: %v", params.ProjectID, err) + return nil, err + } + + // Meta-data for the response + totalCount := *describeTableResult.Table.ItemCount + if int64(len(sigs)) > realPageSize { + sigs = sigs[0:realPageSize] + lastEvaluatedKey = sigs[realPageSize-1].SignatureID + } + + if len(lastEvaluatedKey) > 0 { + log.WithFields(f).Debug("building next key...") + encodedString, err := buildNextKey(indexName, sigs[len(sigs)-1]) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to build nextKey") + } + lastEvaluatedKey = encodedString + log.WithFields(f).Debugf("lastEvaluatedKey encoded is: %s", encodedString) + } + + log.WithFields(f).Debugf("returning %d signatures for CLA Group ID: %s", len(sigs), params.ProjectID) + return &models.Signatures{ + ProjectID: params.ProjectID, + ResultCount: int64(len(sigs)), + TotalCount: totalCount, + LastKeyScanned: lastEvaluatedKey, + Signatures: sigs, + }, nil +} + +// getCLATypeFromParams helper function to combine the new CLA Type parameter and the old legacy signature type parameter - returns one of the values from utils.ClaTypeICLA, utils.ClaTypeECLA, utils.ClaTypeCCLA or empty string if nothing matches +func getCLATypeFromParams(params signatures.GetProjectSignaturesParams) string { + if params.ClaType != nil { + return strings.ToLower(*params.ClaType) + } else if params.SignatureType != nil { + // ICLA -> CLAType == icla, SignatureType == cla + // ECLA -> CLAType == ecla, SignatureType == ecla + // CCLA -> CLAType == ccla, SignatureType == ccla + if strings.ToLower(*params.SignatureType) == "cla" { + return utils.ClaTypeICLA + } + + return strings.ToLower(*params.SignatureType) + } + + return "" +} + +// CreateProjectSummaryReport generates a project summary report based on the specified input +func (repo repository) CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) { // nolint + f := logrus.Fields{ + "functionName": "v1.signatures.repository.CreateProjectSummaryReport", + "tableName": repo.signatureTableName, + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": params.ProjectID, + "signatureType": aws.StringValue(params.SignatureType), + "searchField": aws.StringValue(params.SearchField), + "searchTerm": aws.StringValue(params.SearchTerm), + "fullMatch": aws.BoolValue(params.FullMatch), + "pageSize": aws.Int64Value(params.PageSize), + "nextKey": aws.StringValue(params.NextKey), + "sortOrder": aws.StringValue(params.SortOrder), + "approved": utils.BoolValue(params.Approved), + "signed": utils.BoolValue(params.Signed), + "companyIDList": params.Body, + } + + indexName := SignatureProjectIDIndex + if params.SortOrder != nil && *params.SortOrder != "" { + indexName = SignatureProjectDateIDIndex + } + + realPageSize := int64(100) + if params.PageSize != nil && *params.PageSize > 0 { + realPageSize = *params.PageSize + } + + // This is the key we want to match + condition := expression.Key("signature_project_id").Equal(expression.Value(params.ProjectID)) + + builder := expression.NewBuilder().WithProjection(buildProjection()) + var filter expression.ConditionBuilder + var filterAdded bool + + if params.ClaType != nil { + filterAdded = true + if strings.ToLower(*params.ClaType) == utils.ClaTypeICLA { + log.WithFields(f).Debugf("adding ICLA filters: signature_type: %s, signature_reference_type: %s, signature_user_ccla_company_id: not exists", utils.SignatureTypeCLA, utils.SignatureReferenceTypeUser) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + } else if strings.ToLower(*params.ClaType) == utils.ClaTypeECLA { + log.WithFields(f).Debugf("adding ECLA filters: signature_type: %s, signature_reference_type: %s, signature_user_ccla_company_id: exists", utils.SignatureTypeCLA, utils.SignatureReferenceTypeUser) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeExists(), &filterAdded) + } else if strings.ToLower(*params.ClaType) == utils.ClaTypeCCLA { + log.WithFields(f).Debugf("adding CCLA filters: signature_type: %s, signature_reference_type: %s, signature_user_ccla_company_id: not exists", utils.SignatureTypeCCLA, utils.SignatureReferenceTypeCompany) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + } + } else { + if params.SearchField != nil { + searchFieldExpression := expression.Name("signature_reference_type").Equal(expression.Value(params.SearchField)) + filter = addAndCondition(filter, searchFieldExpression, &filterAdded) + } + + if params.SignatureType != nil { + if params.SearchTerm != nil && (params.FullMatch != nil && !*params.FullMatch) { + indexName = SignatureProjectIDTypeIndex + condition = condition.And(expression.Key("signature_type").Equal(expression.Value(strings.ToLower(*params.SignatureType)))) + } else { + signatureTypeExpression := expression.Name("signature_type").Equal(expression.Value(params.SignatureType)) + filter = addAndCondition(filter, signatureTypeExpression, &filterAdded) + } + if *params.SignatureType == utils.ClaTypeCCLA { + signatureReferenceIDExpression := expression.Name("signature_reference_id").AttributeExists() + signatureUserCclaCompanyIDExpression := expression.Name("signature_user_ccla_company_id").AttributeNotExists() + filter = addAndCondition(filter, signatureReferenceIDExpression, &filterAdded) + filter = addAndCondition(filter, signatureUserCclaCompanyIDExpression, &filterAdded) + } + } + + if params.SearchTerm != nil && utils.StringValue(params.SearchTerm) != "" { + if utils.BoolValue(params.FullMatch) { + indexName = SignatureReferenceSearchIndex + condition = condition.And(expression.Key("signature_reference_name_lower").Equal(expression.Value(strings.ToLower(utils.StringValue(params.SearchTerm))))) + } else { + searchTermExpression := expression.Name("signature_reference_name_lower").Contains(strings.ToLower(utils.StringValue(params.SearchTerm))). + Or(expression.Name("user_email").Contains(strings.ToLower(utils.StringValue(params.SearchTerm)))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + } + } + + if params.Approved != nil { + filterAdded = true + //log.WithFields(f).Debugf("adding filter signature_approved: %t", aws.BoolValue(params.Approved)) + searchTermExpression := expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(params.Approved))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + if params.Signed != nil { + filterAdded = true + //log.WithFields(f).Debugf("adding filter signature_signed: %t", aws.BoolValue(params.Signed)) + searchTermExpression := expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(params.Signed))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if params.Approved == nil && params.Signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } + + if len(params.Body) > 0 { + // expression.Name("Color").In(expression.Value("red"), expression.Value("green"), expression.Value("blue")) + var referenceIDExpressions []expression.OperandBuilder + for _, value := range params.Body { + referenceIDExpressions = append(referenceIDExpressions, expression.Value(value)) + } + if len(referenceIDExpressions) == 1 { + filter = addAndCondition(filter, expression.Name("signature_reference_id").In(referenceIDExpressions[0]), &filterAdded) + } else if len(referenceIDExpressions) > 1 { + filter = addAndCondition(filter, expression.Name("signature_reference_id").In(referenceIDExpressions[0], referenceIDExpressions[1:]...), &filterAdded) + } + } + + if filterAdded { + builder = builder.WithFilter(filter) + } + builder = builder.WithKeyCondition(condition) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project signature query, projectID: %s, error: %v", + params.ProjectID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + Limit: aws.Int64(realPageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) + IndexName: aws.String(indexName), // Name of a secondary index to scan + } + f["indexName"] = indexName + + // If we have the next key, set the exclusive start key value + if params.NextKey != nil { + log.WithFields(f).Debugf("received a nextKey, value: %s", *params.NextKey) + // The primary key of the first item that this operation will evaluate. + // and the query key (if not the same) + queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: params.NextKey, + }, + "signature_project_id": { + S: ¶ms.ProjectID, + }, + } + if params.FullMatch != nil && *params.FullMatch && params.SearchTerm != nil && utils.StringValue(params.SearchTerm) != "" { + queryInput.ExclusiveStartKey["signature_reference_name_lower"] = &dynamodb.AttributeValue{ + S: params.SearchTerm, + } + } + } + + sigs := make([]*models.SignatureSummary, 0) + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + // Make the DynamoDB Query API call + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving project signature ID for project: %s, error: %v", + params.ProjectID, errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + signatureList, modelErr := repo.buildProjectSignatureSummaryModels(ctx, results, params.ProjectID) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s, error: %v", + params.ProjectID, modelErr) + return nil, modelErr + } + + // Add to the signatures response model to the list + sigs = append(sigs, signatureList...) + + //log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey) + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" + } + + if int64(len(sigs)) >= realPageSize { + break + } + } + + // How many total records do we have - may not be up-to-date as this value is updated only periodically + describeTableInput := &dynamodb.DescribeTableInput{ + TableName: &repo.signatureTableName, + } + describeTableResult, err := repo.dynamoDBClient.DescribeTable(describeTableInput) + if err != nil { + log.WithFields(f).Warnf("error retrieving total record count for project: %s, error: %v", params.ProjectID, err) + return nil, err + } + + // Meta-data for the response + totalCount := *describeTableResult.Table.ItemCount + if int64(len(sigs)) > realPageSize { + sigs = sigs[0:realPageSize] + lastEvaluatedKey = sigs[realPageSize-1].SignatureID + } + + return &models.SignatureReport{ + ProjectID: params.ProjectID, + ResultCount: int64(len(sigs)), + TotalCount: totalCount, + LastKeyScanned: lastEvaluatedKey, + Signatures: sigs, + }, nil +} + +// GetProjectCompanySignature returns the signature for the specified project and specified company with the other query flags +func (repo repository) GetProjectCompanySignature(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, pageSize *int64) (*models.Signature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetProjectCompanySignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + "projectID": projectID, + "approved": aws.BoolValue(approved), + "signed": aws.BoolValue(signed), + "pageSize": aws.Int64Value(pageSize), + "nextKey": aws.StringValue(nextKey), + } + + log.WithFields(f).Debug("querying for project company signature...") + sortOrder := utils.SortOrderAscending + sigs, getErr := repo.GetProjectCompanySignatures(ctx, companyID, projectID, signed, approved, nextKey, &sortOrder, pageSize) + if getErr != nil { + log.WithFields(f).WithError(getErr).Warn("problem loading project company signatures...") + return nil, getErr + } + + if sigs == nil || len(sigs.Signatures) == 0 { + return nil, nil + } + + if len(sigs.Signatures) > 1 { + log.WithFields(f).Warnf("more than 1 project company signatures returned in result using company ID: %s, project ID: %s - will return fist record", + companyID, projectID) + } + + log.WithFields(f).Debugf("returning project company signature") + return sigs.Signatures[0], nil +} + +// GetProjectCompanySignatures returns a list of signatures for the specified project and specified company +func (repo repository) GetProjectCompanySignatures(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, sortOrder *string, pageSize *int64) (*models.Signatures, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetProjectCompanySignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + "projectID": projectID, + "nextKey": aws.StringValue(nextKey), + "sortOrder": aws.StringValue(sortOrder), + "pageSize": aws.Int64Value(pageSize), + "approved": utils.BoolValue(approved), + "signed": utils.BoolValue(signed), + } + + var filterAdded bool + // These are the keys we want to match + //condition := expression.Key("signature_project_id").Equal(expression.Value(projectID)) + condition := expression.Key("signature_project_id").Equal(expression.Value(projectID)). + And(expression.Key("signature_reference_id").Equal(expression.Value(companyID))) + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + + if approved != nil { + filterAdded = true + //log.WithFields(f).Debugf("adding filter signature_approved: %t", aws.BoolValue(approved)) + searchTermExpression := expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(approved))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + if signed != nil { + filterAdded = true + //log.WithFields(f).Debugf("adding filter signature_signed: %t", aws.BoolValue(signed)) + searchTermExpression := expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(signed))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if approved == nil && signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } + + limit := int64(10) + if pageSize != nil { + limit = *pageSize + } + log.WithFields(f).Debugf("page size %d", limit) + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter).WithProjection(buildProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project signature ID query, project: %s, error: %v", + projectID, err) + return nil, err + } + + indexName := SignatureProjectReferenceIndex + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String(indexName), // Name of a secondary index to scan + Limit: aws.Int64(limit), + } + + // If we have the next key, set the exclusive start key value + if nextKey != nil { + queryInput.ExclusiveStartKey, err = decodeNextKey(*nextKey) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem decoding next key value") + return nil, err + } + //log.WithFields(f).Debugf("received a nextKey, value: %s - decoded: %+v", *nextKey, queryInput.ExclusiveStartKey) + } + + sigs := make([]*models.Signature, 0) + var lastEvaluatedKey string + + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + // Make the DynamoDB Query API call + // log.WithFields(f).Debugf("executing query for input: %+v", queryInput) + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).WithError(errQuery).Warnf("error retrieving project signature ID for project: %s with company: %s, error: %v", + projectID, companyID, errQuery) + return nil, errQuery + } + log.WithFields(f).Debugf("query response received with %d results", len(results.Items)) + + // If we have any results - may not have any after filters are applied, but may have more records to page through... + if len(results.Items) > 0 { + // Convert the list of DB models to a list of response models + //log.WithFields(f).Debugf("building response model for %d results", len(results.Items)) + signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, projectID, LoadACLDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s with company: %s, error: %v", + projectID, companyID, modelErr) + return nil, modelErr + } + + // Add to the signatures response model to the list + sigs = append(sigs, signatureList...) + } + + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(lastEvaluatedKey), + }, + "signature_project_id": { + S: aws.String(projectID), + }, + "signature_reference_id": { + S: aws.String(companyID), + }, } + } else { + lastEvaluatedKey = "" + } + + if int64(len(sigs)) >= limit { + break + } + } + + // How many total records do we have - may not be up-to-date as this value is updated only periodically + describeTableInput := &dynamodb.DescribeTableInput{ + TableName: &repo.signatureTableName, + } + describeTableResult, err := repo.dynamoDBClient.DescribeTable(describeTableInput) + if err != nil { + log.WithFields(f).Warnf("error retrieving total record count for project: %s, error: %v", projectID, err) + return nil, err + } + + // Calculate the next key - this uses a compound key - need to encode it before sharing with the caller + if len(lastEvaluatedKey) > 0 { + log.WithFields(f).Debug("building next key...") + encodedString, err := buildNextKey(indexName, sigs[len(sigs)-1]) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to build nextKey") } + lastEvaluatedKey = encodedString + //log.WithFields(f).Debugf("lastEvaluatedKey encoded is: %s", encodedString) + } + + // Meta-data for the response + totalCount := *describeTableResult.Table.ItemCount + + return &models.Signatures{ + ProjectID: projectID, + ResultCount: int64(len(sigs)), + TotalCount: totalCount, + LastKeyScanned: lastEvaluatedKey, + Signatures: sigs, + }, nil +} + +// ProjectSignatures - get project signatures with no pagination +func (repo repository) ProjectSignatures(ctx context.Context, projectID string) (*models.Signatures, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.ProjectSignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": projectID, + } + + indexName := SignatureProjectIDIndex + + // This is the key we want to match + condition := expression.Key("signature_project_id").Equal(expression.Value(projectID)) + + builder := expression.NewBuilder().WithProjection(buildProjection()) + var filter expression.ConditionBuilder + var filterAdded bool + + // Filter condition to cater for approved and signed signatures + signatureApprovedExpression := expression.Name("signature_approved").Equal(expression.Value(true)) + filter = addAndCondition(filter, signatureApprovedExpression, &filterAdded) + + signatureSignedExpression := expression.Name("signature_signed").Equal(expression.Value(true)) + filter = addAndCondition(filter, signatureSignedExpression, &filterAdded) + + if filterAdded { + builder = builder.WithFilter(filter) + } + builder = builder.WithKeyCondition(condition) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for project signature query, projectID: %s, error: %v", + projectID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String(indexName), // Name of a secondary index to scan + } + + results, errQuery := repo.dynamoDBClient.Query(queryInput) + + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving project signature ID for project: %s, error: %v", + projectID, errQuery) + return nil, errQuery + } + + // Convert the list of DB models to a list of response models + sigs, modelErr := repo.buildProjectSignatureModels(ctx, results, projectID, LoadACLDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s, error: %v", + projectID, modelErr) + return nil, modelErr + } + + return &models.Signatures{ + ProjectID: projectID, + Signatures: sigs, + }, nil +} + +// InvalidateProjectRecord invalidates the specified project record by setting the signature_approved flag to false +func (repo repository) InvalidateProjectRecord(ctx context.Context, signatureID, note string) error { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.InvalidateProjectRecord", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + } + + // Update project signatures for signature_approved and notes attributes + signatureTableName := fmt.Sprintf("cla-%s-signatures", repo.stage) + + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + updateExpression := "SET " // nolint + + expressionAttributeNames["#A"] = aws.String("signature_approved") + expressionAttributeValues[":a"] = &dynamodb.AttributeValue{BOOL: aws.Bool(false)} + updateExpression = updateExpression + " #A = :a," + + expressionAttributeNames["#S"] = aws.String("note") + expressionAttributeValues[":s"] = &dynamodb.AttributeValue{S: aws.String(note)} + updateExpression = updateExpression + " #S = :s" + + input := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(signatureID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(signatureTableName), + } + + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).Warnf("error updating signature_approved for signature_id : %s error : %v ", signatureID, updateErr) + return updateErr + } + + return nil +} + +// ValidateProjectRecord validates the specified project record by setting the signature_approved flag to true +func (repo repository) ValidateProjectRecord(ctx context.Context, signatureID, note string) error { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.ValidateProjectRecord", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + } + + // Update project signatures for signature_approved and notes attributes + signatureTableName := fmt.Sprintf("cla-%s-signatures", repo.stage) + + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + updateExpression := "SET " // nolint + + expressionAttributeNames["#A"] = aws.String("signature_approved") + expressionAttributeValues[":a"] = &dynamodb.AttributeValue{BOOL: aws.Bool(true)} + updateExpression = updateExpression + " #A = :a," + + expressionAttributeNames["#S"] = aws.String("note") + expressionAttributeValues[":s"] = &dynamodb.AttributeValue{S: aws.String(note)} + updateExpression = updateExpression + " #S = :s" + + input := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(signatureID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(signatureTableName), + } + + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).Warnf("error updating signature_approved for signature_id : %s error : %v ", signatureID, updateErr) + return updateErr + } + + return nil +} + +// GetProjectCompanyEmployeeSignatures returns a list of employee signatures for the specified project and specified company +func (repo repository) GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *ApprovalCriteria) (*models.Signatures, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetProjectCompanyEmployeeSignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.ProjectID, + "companyID": params.CompanyID, + "nextKey": aws.StringValue(params.NextKey), + "sortOrder": aws.StringValue(params.SortOrder), + } + + totalCountChannel := make(chan int64, 1) + go repo.getProjectCompanyEmployeeSignatureCount(ctx, params, criteria, totalCountChannel) + + pageSize := int64(HugePageSize) + if params.PageSize != nil { + pageSize = utils.Int64Value(params.PageSize) + } + f["pageSize"] = pageSize + + // This is the keys we want to match + condition := expression.Key("signature_user_ccla_company_id").Equal(expression.Value(params.CompanyID)).And( + expression.Key("signature_project_id").Equal(expression.Value(params.ProjectID))) - if params.SearchTerm != nil { - if *params.FullMatch { - indexName = SignatureReferenceSearchIndex - condition = condition.And(expression.Key("signature_reference_name_lower").Equal(expression.Value(strings.ToLower(*params.SearchTerm)))) - } else { - searchTermExpression := expression.Name("signature_reference_name_lower").Contains(strings.ToLower(*params.SearchTerm)).Or(expression.Name("user_email").Contains(strings.ToLower(*params.SearchTerm))) - filter = addConditionToFilter(filter, searchTermExpression, &filterAdded) - } - } + var filterAdded bool + var filter expression.ConditionBuilder - // Filter condition to cater for approved and signed signatures - signatureApprovedExpression := expression.Name("signature_approved").Equal(expression.Value(true)) - filter = addConditionToFilter(filter, signatureApprovedExpression, &filterAdded) + if criteria != nil && criteria.GitHubUsername != "" { + //log.WithFields(f).Debugf("adding GitHub username criteria filter for: %s ", criteria.GitHubUsername) + filter = addAndCondition(filter, expression.Name(SignatureUserGitHubUsername).Equal(expression.Value(criteria.GitHubUsername)), &filterAdded) + } - signatureSignedExpression := expression.Name("signature_signed").Equal(expression.Value(true)) - filter = addConditionToFilter(filter, signatureSignedExpression, &filterAdded) + if criteria != nil && criteria.GitlabUsername != "" { + //log.WithFields(f).Debugf("adding GitLab username criteria filter for :%s ", criteria.GitlabUsername) + filter = addAndCondition(filter, expression.Name(SignatureUserGitlabUsername).Equal(expression.Value(criteria.GitlabUsername)), &filterAdded) } - if filterAdded { - builder = builder.WithFilter(filter) + if criteria != nil && criteria.UserEmail != "" { + //log.WithFields(f).Debugf("adding useremail criteria filter for : %s ", criteria.UserEmail) + filter = addAndCondition(filter, expression.Name("user_email").Equal(expression.Value(criteria.UserEmail)), &filterAdded) + } + + if params.SearchTerm != nil { + log.WithFields(f).Debugf("adding search term criteria filter for : %s ", *params.SearchTerm) + searchExpression := expression.Name("user_name").Contains(*params.SearchTerm). + Or(expression.Name("user_email").Contains(*params.SearchTerm)). + Or(expression.Name("user_github_username").Contains(*params.SearchTerm)). + Or(expression.Name("user_gitlab_username").Contains(*params.SearchTerm)). + Or(expression.Name("user_lf_username").Contains(*params.SearchTerm)) + filter = addAndCondition(filter, searchExpression, &filterAdded) } - builder = builder.WithKeyCondition(condition) + beforeQuery, _ := utils.CurrentTime() + //log.WithFields(f).Debugf("running signature query on table: %s", repo.signatureTableName) // Use the nice builder to create the expression - expr, err := builder.Build() + expressionBuilder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()) + if filterAdded { + expressionBuilder = expressionBuilder.WithFilter(filter) + } + expr, err := expressionBuilder.Build() if err != nil { - log.WithFields(f).Warnf("error building expression for project signature query, projectID: %s, error: %v", + log.WithFields(f).Warnf("error building expression for project signature ID query, project: %s, error: %v", params.ProjectID, err) return nil, err } @@ -776,31 +2223,31 @@ func (repo repository) GetProjectSignatures(ctx context.Context, params signatur ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), - FilterExpression: expr.Filter(), TableName: aws.String(repo.signatureTableName), - Limit: aws.Int64(realPageSize), // The maximum number of items to evaluate (not necessarily the number of matching items) - IndexName: aws.String(indexName), // Name of a secondary index to scan + IndexName: aws.String("signature-user-ccla-company-index"), // Name of a secondary index to scan + Limit: aws.Int64(pageSize), + } + + if filterAdded { + queryInput.FilterExpression = expr.Filter() } - f["indexName"] = indexName // If we have the next key, set the exclusive start key value if params.NextKey != nil { - log.WithFields(f).Debugf("received a nextKey, value: %s", *params.NextKey) + log.WithFields(f).Debugf("Received a nextKey, value: %s", *params.NextKey) // The primary key of the first item that this operation will evaluate. // and the query key (if not the same) queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ "signature_id": { S: params.NextKey, }, + "signature_user_ccla_company_id": { + S: ¶ms.CompanyID, + }, "signature_project_id": { S: ¶ms.ProjectID, }, } - if params.FullMatch != nil && *params.FullMatch && params.SearchTerm != nil { - queryInput.ExclusiveStartKey["signature_reference_name_lower"] = &dynamodb.AttributeValue{ - S: params.SearchTerm, - } - } } sigs := make([]*models.Signature, 0) @@ -809,26 +2256,24 @@ func (repo repository) GetProjectSignatures(ctx context.Context, params signatur // Loop until we have all the records for ok := true; ok; ok = lastEvaluatedKey != "" { // Make the DynamoDB Query API call - log.WithFields(f).Debugf("Running signature project query using queryInput: %+v", queryInput) results, errQuery := repo.dynamoDBClient.Query(queryInput) if errQuery != nil { - log.WithFields(f).Warnf("error retrieving project signature ID for project: %s, error: %v", - params.ProjectID, errQuery) + log.WithFields(f).Warnf("error retrieving project company employee signature ID for project: %s with company: %s, error: %v", + params.ProjectID, params.CompanyID, errQuery) return nil, errQuery } // Convert the list of DB models to a list of response models signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, params.ProjectID, LoadACLDetails) if modelErr != nil { - log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s, error: %v", - params.ProjectID, modelErr) + log.WithFields(f).Warnf("error converting DB model to response model for employee signatures with project %s with company: %s, error: %v", + params.ProjectID, params.CompanyID, modelErr) return nil, modelErr } - // Add to the signatures response model to the list + // Add to the signature response model to the list sigs = append(sigs, signatureList...) - //log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey) if results.LastEvaluatedKey["signature_id"] != nil { lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S queryInput.ExclusiveStartKey = results.LastEvaluatedKey @@ -836,28 +2281,22 @@ func (repo repository) GetProjectSignatures(ctx context.Context, params signatur lastEvaluatedKey = "" } - if int64(len(sigs)) >= realPageSize { + if int64(len(sigs)) >= pageSize { break } } + log.WithFields(f).Debugf("finished signature query on table: %s - duration: %+v", repo.signatureTableName, time.Since(beforeQuery)) - // How many total records do we have - may not be up-to-date as this value is updated only periodically - describeTableInput := &dynamodb.DescribeTableInput{ - TableName: &repo.signatureTableName, - } - describeTableResult, err := repo.dynamoDBClient.DescribeTable(describeTableInput) - if err != nil { - log.WithFields(f).Warnf("error retrieving total record count for project: %s, error: %v", params.ProjectID, err) - return nil, err - } + // remove duplicate values + sigs = getLatestSignatures(sigs) // Meta-data for the response - totalCount := *describeTableResult.Table.ItemCount - if int64(len(sigs)) > realPageSize { - sigs = sigs[0:realPageSize] - lastEvaluatedKey = sigs[realPageSize-1].SignatureID.String() + if int64(len(sigs)) > pageSize { + sigs = sigs[0:pageSize] + lastEvaluatedKey = sigs[pageSize-1].SignatureID } + totalCount := <-totalCountChannel return &models.Signatures{ ProjectID: params.ProjectID, ResultCount: int64(len(sigs)), @@ -867,79 +2306,84 @@ func (repo repository) GetProjectSignatures(ctx context.Context, params signatur }, nil } -// GetProjectCompanySignature returns a the signature for the specified project and specified company with the other query flags -func (repo repository) GetProjectCompanySignature(ctx context.Context, companyID, projectID string, signed, approved *bool, nextKey *string, pageSize *int64) (*models.Signature, error) { +func getLatestSignatures(signatures []*models.Signature) []*models.Signature { f := logrus.Fields{ - "functionName": "GetProjectCompanySignature", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companyID": companyID, - "projectID": projectID, - "approved": aws.BoolValue(approved), - "signed": aws.BoolValue(signed), - "pageSize": aws.Int64Value(pageSize), - "nextKey": aws.StringValue(nextKey), - } - sortOrder := utils.SortOrderAscending - sigs, getErr := repo.GetProjectCompanySignatures(ctx, companyID, projectID, signed, approved, nextKey, &sortOrder, pageSize) - if getErr != nil { - return nil, getErr + "functionName": "v1.signatures.repository.getLatestSignatures", } - if sigs == nil || sigs.Signatures == nil { - return nil, nil + signatureMap := make(map[string]*models.Signature) + result := []*models.Signature{} + + log.WithFields(f).Debug("get latest signatures per contributor...") + + for _, signature := range signatures { + if _, ok := signatureMap[signature.SignatureReferenceID]; !ok { + log.WithFields(f).Debugf("adding signature: %s to map", signature.SignatureReferenceID) + signatureMap[signature.SignatureReferenceID] = signature + } else { + if signature.Modified > signatureMap[signature.SignatureReferenceID].Modified { + signatureMap[signature.SignatureReferenceID] = signature + } + } } - if len(sigs.Signatures) > 1 { - log.WithFields(f).Warnf("more than 1 project company signatures returned in result using company ID: %s, project ID: %s - will return fist record", - companyID, projectID) + log.WithFields(f).Debugf("signature Map: %+v", signatureMap) + + for _, signature := range signatureMap { + result = append(result, signature) } - return sigs.Signatures[0], nil + return result } -// GetProjectCompanySignatures returns a list of signatures for the specified project and specified company -func (repo repository) GetProjectCompanySignatures(ctx context.Context, companyID, projectID string, signed, approved *bool, nextKey *string, sortOrder *string, pageSize *int64) (*models.Signatures, error) { +type EmployeeModel struct { + Signature *models.Signature + User *models.User +} + +func (repo repository) GetProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User, wg *sync.WaitGroup, resultChannel chan<- *EmployeeModel, errorChannel chan<- error) { + defer wg.Done() + f := logrus.Fields{ - "functionName": "GetProjectCompanySignatures", + "functionName": "v1.signatures.repository.GetProjectCompanyEmployeeSignature", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companyID": companyID, - "projectID": projectID, - "signed": aws.BoolValue(signed), - "approved": aws.BoolValue(approved), - "nextKey": aws.StringValue(nextKey), - "sortOrder": aws.StringValue(sortOrder), - "pageSize": aws.Int64Value(pageSize), } - - // These are the keys we want to match - condition := expression.Key("signature_project_id").Equal(expression.Value(projectID)) - filter := expression.Name("signature_reference_id").Equal(expression.Value(companyID)). - And(expression.Name("signature_type").Equal(expression.Value("ccla"))). - And(expression.Name("signature_reference_type").Equal(expression.Value("company"))) - - // If the caller provided a signature signed value...add the appropriate filter - if signed != nil { - log.WithFields(f).Debugf("Filtering signature_signed: %+v", *signed) - filter = filter.And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(*signed)))) + if claGroupModel != nil { + f["projectID"] = claGroupModel.ProjectID + f["projectName"] = claGroupModel.ProjectName } - - // If the caller provided a signature approved value...add the appropriate filter - if approved != nil { - log.WithFields(f).Debugf("Filter by signature_approved: %+v", *approved) - filter = filter.And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(*approved)))) + if companyModel != nil { + f["companyID"] = companyModel.CompanyID + f["companyName"] = companyModel.CompanyName + } + if employeeUserModel != nil { + f["employeeUserID"] = employeeUserModel.UserID + f["employeeUserName"] = employeeUserModel.Username + f["employeeEmails"] = strings.Join(employeeUserModel.Emails, ",") } - limit := int64(10) - if pageSize != nil { - limit = *pageSize + if companyModel == nil || claGroupModel == nil || employeeUserModel == nil { + resultChannel <- nil + return } - // Use the nice builder to create the expression + // This is the keys we want to match + condition := expression.Key("signature_reference_id").Equal(expression.Value(employeeUserModel.UserID)) + + var filterAdded bool + var filter expression.ConditionBuilder + + // Check for approved signatures + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").Equal(expression.Value(companyModel.CompanyID)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_project_id").Equal(expression.Value(claGroupModel.ProjectID)), &filterAdded) + + log.WithFields(f).Debugf("running employee signature query on table: %s", repo.signatureTableName) expr, err := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter).WithProjection(buildProjection()).Build() if err != nil { - log.WithFields(f).Warnf("error building expression for project signature ID query, project: %s, error: %v", - projectID, err) - return nil, err + log.WithFields(f).WithError(err).Warnf("error building expression for employee signature query, company model: %+v, CLA group model: %+v, employee model: %+v", + companyModel, claGroupModel, employeeUserModel) + errorChannel <- err + return } // Assemble the query input parameters @@ -950,342 +2394,303 @@ func (repo repository) GetProjectCompanySignatures(ctx context.Context, companyI FilterExpression: expr.Filter(), ProjectionExpression: expr.Projection(), TableName: aws.String(repo.signatureTableName), - IndexName: aws.String("project-signature-index"), // Name of a secondary index to scan - Limit: aws.Int64(limit), + IndexName: aws.String("reference-signature-index"), // Name of a secondary index to scan + Limit: aws.Int64(10), } - // If we have the next key, set the exclusive start key value - if nextKey != nil { - log.WithFields(f).Debugf("Received a nextKey, value: %s", *nextKey) - // The primary key of the first item that this operation will evaluate. - // and the query key (if not the same) - queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "signature_id": { - S: nextKey, - }, - "signature_project_id": { - S: &projectID, - }, - } + // Make the DynamoDB Query API call + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).WithError(errQuery).Warnf("error retrieving project company employee acknowledgement record for company model: %+v, CLA group model: %+v, employee model: %+v", + companyModel, claGroupModel, employeeUserModel) + errorChannel <- errQuery + return } - var sigs []*models.Signature - var lastEvaluatedKey string - - // Loop until we have all the records - for ok := true; ok; ok = lastEvaluatedKey != "" { - // Make the DynamoDB Query API call - results, errQuery := repo.dynamoDBClient.Query(queryInput) - if errQuery != nil { - log.WithFields(f).Warnf("error retrieving project signature ID for project: %s with company: %s, error: %v", - projectID, companyID, errQuery) - return nil, errQuery - } - - // Convert the list of DB models to a list of response models - signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, projectID, LoadACLDetails) - if modelErr != nil { - log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s with company: %s, error: %v", - projectID, companyID, modelErr) - return nil, modelErr - } - - // Add to the signatures response model to the list - sigs = append(sigs, signatureList...) - - // log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey["signature_id"]) - if results.LastEvaluatedKey["signature_id"] != nil { - lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S - queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "signature_id": { - S: aws.String(lastEvaluatedKey), - }, - "signature_project_id": { - S: &projectID, - }, - } - } else { - lastEvaluatedKey = "" - } - - if int64(len(sigs)) >= limit { - break + if results == nil || len(results.Items) == 0 { + log.WithFields(f).Debug("No ecla records found!") + resultChannel <- &EmployeeModel{ + Signature: nil, + User: employeeUserModel, } + return } - - // How many total records do we have - may not be up-to-date as this value is updated only periodically - describeTableInput := &dynamodb.DescribeTableInput{ - TableName: &repo.signatureTableName, + log.WithFields(f).Debugf("returned %d results", len(results.Items)) + // Convert the list of DB models to a list of response models + signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, claGroupModel.ProjectID, LoadACLDetails) + if modelErr != nil { + log.WithFields(f).WithError(modelErr).Warnf("error converting DB model to response model for project company employee acknowledgement record for company model: %+v, CLA group model: %+v, employee model: %+v", + companyModel, claGroupModel, employeeUserModel) + errorChannel <- modelErr + return } - describeTableResult, err := repo.dynamoDBClient.DescribeTable(describeTableInput) - if err != nil { - log.WithFields(f).Warnf("error retrieving total record count for project: %s, error: %v", projectID, err) - return nil, err + + if len(signatureList) == 0 { + resultChannel <- nil + return } - // Meta-data for the response - totalCount := *describeTableResult.Table.ItemCount + if len(signatureList) > 1 { + log.WithFields(f).Warnf("found more than one signature for employee company model: %+v, CLA group model: %+v, employee model: %+v", + companyModel, claGroupModel, employeeUserModel) + } - return &models.Signatures{ - ProjectID: projectID, - ResultCount: int64(len(sigs)), - TotalCount: totalCount, - LastKeyScanned: lastEvaluatedKey, - Signatures: sigs, - }, nil + resultChannel <- &EmployeeModel{ + Signature: signatureList[0], + User: employeeUserModel, + } } -// Get project signatures with no pagination -func (repo repository) ProjectSignatures(ctx context.Context, projectID string) (*models.Signatures, error) { +// CreateProjectCompanyEmployeeSignature creates a new project employee signature using the provided details +func (repo repository) CreateProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User) error { f := logrus.Fields{ - "functionName": "ProjectSignatures", + "functionName": "v1.signatures.repository.CreateProjectCompanyEmployeeSignature", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectID": projectID, } - - indexName := SignatureProjectIDIndex - - // This is the key we want to match - condition := expression.Key("signature_project_id").Equal(expression.Value(projectID)) - - builder := expression.NewBuilder().WithProjection(buildProjection()) - var filter expression.ConditionBuilder - var filterAdded bool - - // Filter condition to cater for approved and signed signatures - signatureApprovedExpression := expression.Name("signature_approved").Equal(expression.Value(true)) - filter = addConditionToFilter(filter, signatureApprovedExpression, &filterAdded) - - signatureSignedExpression := expression.Name("signature_signed").Equal(expression.Value(true)) - filter = addConditionToFilter(filter, signatureSignedExpression, &filterAdded) - - if filterAdded { - builder = builder.WithFilter(filter) - } - builder = builder.WithKeyCondition(condition) - - // Use the nice builder to create the expression - expr, err := builder.Build() - if err != nil { - log.WithFields(f).Warnf("error building expression for project signature query, projectID: %s, error: %v", - projectID, err) - return nil, err - } - - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - KeyConditionExpression: expr.KeyCondition(), - ProjectionExpression: expr.Projection(), - FilterExpression: expr.Filter(), - TableName: aws.String(repo.signatureTableName), - IndexName: aws.String(indexName), // Name of a secondary index to scan + if claGroupModel != nil { + f["projectID"] = claGroupModel.ProjectID + f["projectName"] = claGroupModel.ProjectName } - - results, errQuery := repo.dynamoDBClient.Query(queryInput) - - if errQuery != nil { - log.WithFields(f).Warnf("error retrieving project signature ID for project: %s, error: %v", - projectID, errQuery) - return nil, errQuery + if companyModel != nil { + f["companyID"] = companyModel.CompanyID + f["companyName"] = companyModel.CompanyName } - - // Convert the list of DB models to a list of response models - sigs, modelErr := repo.buildProjectSignatureModels(ctx, results, projectID, LoadACLDetails) - if modelErr != nil { - log.WithFields(f).Warnf("error converting DB model to response model for signatures with project %s, error: %v", - projectID, modelErr) - return nil, modelErr + if employeeUserModel != nil { + f["employeeUserID"] = employeeUserModel.UserID + f["employeeUserName"] = employeeUserModel.Username + f["employeeEmails"] = strings.Join(employeeUserModel.Emails, ",") } - return &models.Signatures{ - ProjectID: projectID, - Signatures: sigs, - }, nil -} + var wg sync.WaitGroup + resultChan := make(chan *EmployeeModel) + errorChan := make(chan error) + + wg.Add(1) + go repo.GetProjectCompanyEmployeeSignature(ctx, companyModel, claGroupModel, employeeUserModel, &wg, resultChan, errorChan) + + go func() { + wg.Wait() + close(resultChan) + close(errorChan) + }() + + for result := range resultChan { + if result != nil { + existingSig := result.Signature + // If exists, need to update + if existingSig != nil { + log.WithFields(f).Debug("found existing employee acknowledgement") + if !existingSig.SignatureApproved { + log.WithFields(f).Debugf("found existing employee acknowledgement, but not currently approved.") + validateRecordErr := repo.ValidateProjectRecord(ctx, existingSig.SignatureID, fmt.Sprintf(" Enabled previously disabled employee acknowledgement via CLA Manager approval list edit with auto-enable feature flag configured on %s.", utils.CurrentSimpleDateTimeString())) + if validateRecordErr != nil { + return validateRecordErr + } + return nil + } -// InvalidateProjectRecord invalidates the specified project record by setting the signature_approved flag to false -func (repo repository) InvalidateProjectRecord(ctx context.Context, signatureID string, projectName string) error { - f := logrus.Fields{ - "functionName": "InvalidateProjectRecord", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "signatureID": signatureID, + return nil + } + } } - // Update project signatures for signature_approved and notes attributes - signatureTableName := fmt.Sprintf("cla-%s-signatures", repo.stage) + for err := range errorChan { + if err != nil { + log.WithFields(f).WithError(err).Warnf("error creating project company employee signature for company model: %+v, CLA group model: %+v, employee model: %+v", + companyModel, claGroupModel, employeeUserModel) + } + } - expressionAttributeNames := map[string]*string{} - expressionAttributeValues := map[string]*dynamodb.AttributeValue{} - updateExpression := "SET " // nolint + log.WithFields(f).Debugf("creating project company employee signature for project: %+v, company: %+v, employee: %+v", claGroupModel, companyModel, employeeUserModel) - expressionAttributeNames["#A"] = aws.String("signature_approved") - expressionAttributeValues[":a"] = &dynamodb.AttributeValue{BOOL: aws.Bool(false)} - updateExpression = updateExpression + " #A = :a," + // If not exists, need to create + // No existing records - create one + _, currentTime := utils.CurrentTime() + newSignatureID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warnf("Unable to generate a UUID for signature record") + return err + } - expressionAttributeNames["#S"] = aws.String("note") - note := fmt.Sprintf("Signature invalidated (approved set to false) due to CLA Group/Project: %s deletion", projectName) - expressionAttributeValues[":s"] = &dynamodb.AttributeValue{S: aws.String(note)} - updateExpression = updateExpression + " #S = :s" + newSignature := &SignatureDynamoDB{ + SignatureID: newSignatureID.String(), + SignatureProjectID: claGroupModel.ProjectID, + AutoCreateECLA: false, + SignatureType: utils.ClaTypeECLA, + ProjectName: claGroupModel.ProjectName, + ProjectSFID: claGroupModel.ProjectExternalID, + CompanyName: companyModel.CompanyName, + CompanyID: companyModel.CompanyID, + CompanySFID: companyModel.CompanyExternalID, + SignatureUserCCLACompanyID: companyModel.CompanyID, + ProjectID: claGroupModel.ProjectID, + SignatureReferenceID: employeeUserModel.UserID, + SignatureApproved: true, + SignatureSigned: true, + SignatureDocumentMajorVersion: 2, + SignatureDocumentMinorVersion: 0, + SigTypeSignedApprovedID: fmt.Sprintf("ecla#true#true#%s", companyModel.CompanyID), + SignatureReferenceType: utils.SignatureReferenceTypeUser, + SignatureACL: []string{}, + SignedOn: currentTime, + DateCreated: currentTime, + DateModified: currentTime, + Version: "v1", + UserGitHubUsername: employeeUserModel.GithubUsername, + UserGitLabUsername: employeeUserModel.GitlabUsername, + Note: fmt.Sprintf("automatically created employee ackowledgement via CLA Manager approval list edit/update with auto_create_ecla feature flag set to true on %+v.", currentTime), + } + + // Try to figure out the employee's name + // Signature Reference Name fields MUST have a value - cannot be nil because it is indexed (we have a separate index for these columns) + employeeUserName := employeeUserModel.Username + if employeeUserName == "" { + if employeeUserModel.LfUsername != "" { + employeeUserName = employeeUserModel.LfUsername + } else if employeeUserModel.GithubUsername != "" { + employeeUserName = employeeUserModel.GithubUsername + } else if employeeUserModel.GitlabUsername != "" { + employeeUserName = employeeUserModel.GitlabUsername + } else if employeeUserModel.LfEmail != "" { + employeeUserName = employeeUserModel.LfEmail.String() + } else if employeeUserModel.Emails != nil && len(employeeUserModel.Emails) > 0 { + employeeUserName = employeeUserModel.Emails[0] + } + } + if employeeUserName != "" { + newSignature.SignatureReferenceName = employeeUserName + newSignature.SignatureReferenceNameLower = strings.ToLower(employeeUserName) + } - input := &dynamodb.UpdateItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "signature_id": { - S: aws.String(signatureID), - }, - }, - ExpressionAttributeNames: expressionAttributeNames, - ExpressionAttributeValues: expressionAttributeValues, - UpdateExpression: &updateExpression, - TableName: aws.String(signatureTableName), + av, marshalErr := dynamodbattribute.MarshalMap(newSignature) + if marshalErr != nil { + log.WithFields(f).WithError(marshalErr).Warn("unable to create new signature record") + return marshalErr } - _, updateErr := repo.dynamoDBClient.UpdateItem(input) - if updateErr != nil { - log.WithFields(f).Warnf("error updating signature_approved for signature_id : %s error : %v ", signatureID, updateErr) - return updateErr + _, putErr := repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(repo.signatureTableName), + }) + if putErr != nil { + log.WithFields(f).WithError(putErr).Warn("cannot create new signature record") + return putErr } return nil } -// GetProjectCompanyEmployeeSignatures returns a list of employee signatures for the specified project and specified company -func (repo repository) GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, pageSize int64) (*models.Signatures, error) { +// getProjectCompanyEmployeeSignatureCount returns the total count of employee signatures for the specified project and specified company +func (repo repository) getProjectCompanyEmployeeSignatureCount(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *ApprovalCriteria, responseChannel chan int64) { f := logrus.Fields{ - "functionName": "GetProjectCompanyEmployeeSignatures", + "functionName": "v1.signatures.repository.getProjectCompanyEmployeeSignatureCount", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectID": params.ProjectID, "companyID": params.CompanyID, "nextKey": aws.StringValue(params.NextKey), "sortOrder": aws.StringValue(params.SortOrder), - "pageSize": aws.Int64Value(params.PageSize), } + // Ignore the provided page count in the parameters - we're focused on getting the total count + pageSize := int64(HugePageSize) + f["pageSize"] = pageSize + // This is the keys we want to match condition := expression.Key("signature_user_ccla_company_id").Equal(expression.Value(params.CompanyID)).And( expression.Key("signature_project_id").Equal(expression.Value(params.ProjectID))) - // Check for approved signatures - filter := expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))) + var filterAdded bool + var filter expression.ConditionBuilder + + if criteria != nil && criteria.GitHubUsername != "" { + filter = addAndCondition(filter, expression.Name(SignatureUserGitHubUsername).Equal(expression.Value(criteria.GitHubUsername)), &filterAdded) + } + + if criteria != nil && criteria.GitHubUsername != "" { + filter = addAndCondition(filter, expression.Name(SignatureUserGitlabUsername).Equal(expression.Value(criteria.GitlabUsername)), &filterAdded) + } + + if criteria != nil && criteria.UserEmail != "" { + filter = addAndCondition(filter, expression.Name("user_email").Equal(expression.Value(criteria.UserEmail)), &filterAdded) + } + + beforeQuery, _ := utils.CurrentTime() + log.WithFields(f).Debugf("running total signature count query on table: %s", repo.signatureTableName) // Use the nice builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter).WithProjection(buildProjection()).Build() + expressionBuilder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildCountProjection()) + if filterAdded { + expressionBuilder = expressionBuilder.WithFilter(filter) + } + expr, err := expressionBuilder.Build() if err != nil { log.WithFields(f).Warnf("error building expression for project signature ID query, project: %s, error: %v", params.ProjectID, err) - return nil, err + responseChannel <- 0 + return } - // Assemble the query input parameters + // Assemble the query input parameters - ignore the provided exclusive start key, we're only interested in the total count queryInput := &dynamodb.QueryInput{ ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.KeyCondition(), - FilterExpression: expr.Filter(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.signatureTableName), - IndexName: aws.String("signature-user-ccla-company-index"), // Name of a secondary index to scan + // FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String("signature-user-ccla-company-index"), // Name of a secondary index to scan + Limit: aws.Int64(pageSize), } - // If we have the next key, set the exclusive start key value - if params.NextKey != nil { - log.WithFields(f).Debugf("Received a nextKey, value: %s", *params.NextKey) - // The primary key of the first item that this operation will evaluate. - // and the query key (if not the same) - queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ - "signature_id": { - S: params.NextKey, - }, - "signature_user_ccla_company_id": { - S: ¶ms.CompanyID, - }, - "signature_project_id": { - S: ¶ms.ProjectID, - }, - } + if filterAdded { + queryInput.FilterExpression = expr.Filter() } - sigs := make([]*models.Signature, 0) var lastEvaluatedKey string + var totalCount int64 // Loop until we have all the records for ok := true; ok; ok = lastEvaluatedKey != "" { // Make the DynamoDB Query API call - //log.WithFields(f).Debugf("Running signature project company query using queryInput: %+v", queryInput) results, errQuery := repo.dynamoDBClient.Query(queryInput) if errQuery != nil { log.WithFields(f).Warnf("error retrieving project company employee signature ID for project: %s with company: %s, error: %v", params.ProjectID, params.CompanyID, errQuery) - return nil, errQuery + responseChannel <- 0 + return } - // Convert the list of DB models to a list of response models - signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, params.ProjectID, LoadACLDetails) - if modelErr != nil { - log.WithFields(f).Warnf("error converting DB model to response model for employee signatures with project %s with company: %s, error: %v", - params.ProjectID, params.CompanyID, modelErr) - return nil, modelErr - } - - // Add to the signatures response model to the list - sigs = append(sigs, signatureList...) + // Add to our total count + totalCount += *results.Count - // log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey["signature_id"]) if results.LastEvaluatedKey["signature_id"] != nil { lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S queryInput.ExclusiveStartKey = results.LastEvaluatedKey } else { lastEvaluatedKey = "" } - - if int64(len(sigs)) >= pageSize { - break - } - } - - // How many total records do we have - may not be up-to-date as this value is updated only periodically - describeTableInput := &dynamodb.DescribeTableInput{ - TableName: &repo.signatureTableName, - } - describeTableResult, err := repo.dynamoDBClient.DescribeTable(describeTableInput) - if err != nil { - log.WithFields(f).Warnf("error retrieving total record count for project: %s, error: %v", params.ProjectID, err) - return nil, err - } - - // Meta-data for the response - totalCount := *describeTableResult.Table.ItemCount - if int64(len(sigs)) > pageSize { - sigs = sigs[0:pageSize] - lastEvaluatedKey = sigs[pageSize-1].SignatureID.String() } + log.WithFields(f).Debugf("finished signature total count query on table: %s - duration: %+v", repo.signatureTableName, time.Since(beforeQuery)) - return &models.Signatures{ - ProjectID: params.ProjectID, - ResultCount: int64(len(sigs)), - TotalCount: totalCount, - LastKeyScanned: lastEvaluatedKey, - Signatures: sigs, - }, nil + responseChannel <- totalCount } // GetCompanySignatures returns a list of company signatures for the specified company func (repo repository) GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams, pageSize int64, loadACL bool) (*models.Signatures, error) { f := logrus.Fields{ - "functionName": "GetCompanySignatures", + "functionName": "v1.signatures.repository.GetCompanySignatures", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } // This is the keys we want to match condition := expression.Key("signature_reference_id").Equal(expression.Value(params.CompanyID)) - // Check for approved signatures - filter := expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))) + var filterAdded bool + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) if params.SignatureType != nil { - filter = filter.And(expression.Name("signature_type").Equal(expression.Value(*params.SignatureType))) + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(*params.SignatureType)), &filterAdded) } // Use the nice builder to create the expression @@ -1373,7 +2778,7 @@ func (repo repository) GetCompanySignatures(ctx context.Context, params signatur } if int64(len(sigs)) > pageSize { sigs = sigs[0:pageSize] - lastEvaluatedKey = sigs[pageSize-1].SignatureID.String() + lastEvaluatedKey = sigs[pageSize-1].SignatureID } // Meta-data for the response @@ -1391,7 +2796,7 @@ func (repo repository) GetCompanySignatures(ctx context.Context, params signatur // GetCompanyIDsWithSignedCorporateSignatures returns a list of company IDs that have signed a CLA agreement func (repo repository) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]SignatureCompanyID, error) { f := logrus.Fields{ - "functionName": "GetCompanyIDsWithSignedCorporateSignatures", + "functionName": "v1.signatures.repository.GetCompanyIDsWithSignedCorporateSignatures", "claGroupID": claGroupID, "signature_project_id": claGroupID, "signature_type": "ccla", @@ -1404,10 +2809,16 @@ func (repo repository) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Co // These are the keys we want to match condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)) - filter := expression.Name("signature_type").Equal(expression.Value("ccla")). - And(expression.Name("signature_reference_type").Equal(expression.Value("company"))). - And(expression.Name("signature_signed").Equal(expression.Value(aws.Bool(true)))). - And(expression.Name("signature_approved").Equal(expression.Value(aws.Bool(true)))) + + var filterAdded bool + var filter expression.ConditionBuilder + + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeCompany)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + // Check for approved signatures + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) // Batch size limit := int64(100) @@ -1471,16 +2882,27 @@ func (repo repository) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Co } // GetUserSignatures returns a list of user signatures for the specified user -func (repo repository) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64) (*models.Signatures, error) { +func (repo repository) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64, projectID *string) (*models.Signatures, error) { f := logrus.Fields{ - "functionName": "GetUserSignatures", + "functionName": "v1.signatures.repository.GetUserSignatures", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } // This is the keys we want to match condition := expression.Key("signature_reference_id").Equal(expression.Value(params.UserID)) + filterExpression := expression.Name("signature_user_ccla_company_id").AttributeNotExists() + + // Check for approved signatures + expressionBuilder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()) + + if projectID != nil { + filterExpression = filterExpression.And(expression.Name("signature_project_id").Equal(expression.Value(*projectID))) + } + + expressionBuilder = expressionBuilder.WithFilter(filterExpression) + // Use the nice builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + expr, err := expressionBuilder.Build() if err != nil { log.WithFields(f).Warnf("error building expression for user signature query, userID: %s, error: %v", params.UserID, err) @@ -1527,6 +2949,8 @@ func (repo repository) GetUserSignatures(ctx context.Context, params signatures. return nil, errQuery } + log.WithFields(f).Debugf("query results count: %d", len(results.Items)) + // Convert the list of DB models to a list of response models signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, "", LoadACLDetails) if modelErr != nil { @@ -1576,7 +3000,7 @@ func (repo repository) GetUserSignatures(ctx context.Context, params signatures. func (repo repository) AddCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) { f := logrus.Fields{ - "functionName": "AddCLAManager", + "functionName": "v1.signatures.repository.AddCLAManager", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, "claManagerID": claManagerID, @@ -1644,7 +3068,7 @@ func (repo repository) AddCLAManager(ctx context.Context, signatureID, claManage func (repo repository) RemoveCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) { f := logrus.Fields{ - "functionName": "RemoveCLAManager", + "functionName": "v1.signatures.repository.RemoveCLAManager", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, "claManagerID": claManagerID, @@ -1717,133 +3141,616 @@ func (repo repository) RemoveCLAManager(ctx context.Context, signatureID, claMan } // UpdateApprovalList updates the specified project/company signature with the updated approval list information -func (repo repository) UpdateApprovalList(ctx context.Context, projectID, companyID string, params *models.ApprovalList) (*models.Signature, error) { // nolint +func (repo repository) UpdateApprovalList(ctx context.Context, claManager *models.User, claGroupModel *models.ClaGroup, companyID string, params *models.ApprovalList, eventArgs *events.LogEventArgs) (*models.Signature, error) { // nolint + + projectID := claGroupModel.ProjectID f := logrus.Fields{ - "functionName": "UpdateApprovalList", + "functionName": "v1.signatures.repository.UpdateApprovalList", "projectID": projectID, "companyID": companyID, } log.WithFields(f).Debug("querying database for approval list details") - signed, approved := true, true - pageSize := int64(10) - log.WithFields(f).Debugf("querying database for approval list details using company ID: %s project ID: %s, type: ccla, signed: true, approved: true", - companyID, projectID) - sortOrder := utils.SortOrderAscending - sigs, sigErr := repo.GetProjectCompanySignatures(ctx, companyID, projectID, &signed, &approved, nil, &sortOrder, &pageSize) - if sigErr != nil { - return nil, sigErr - } + approved, signed := true, true - if sigs == nil || sigs.Signatures == nil { - msg := fmt.Sprintf("unable to locate signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - companyID, projectID, signed, approved) + // Get CCLA signature - For Approval List info + cclaSignature, err := repo.GetCorporateSignature(ctx, projectID, companyID, &approved, &signed) + if err != nil || cclaSignature == nil { + msg := fmt.Sprintf("unable to get corporate signature for CLA Group: %s and company: %s", projectID, companyID) log.WithFields(f).Warn(msg) return nil, errors.New(msg) } - if len(sigs.Signatures) > 1 { - log.WithFields(f).Warnf("more than 1 CCLA signature returned for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t - expecting zero or 1 - using first record", - companyID, projectID, signed, approved) + signatureID := cclaSignature.SignatureID + + // Get CLA Manager + var cclaManagers []ClaManagerInfoParams + for i := range cclaSignature.SignatureACL { + cclaManagers = append(cclaManagers, ClaManagerInfoParams{ + Username: utils.GetBestUsername(&cclaSignature.SignatureACL[i]), + Email: getBestEmail(&cclaSignature.SignatureACL[i]), + }) + } + + // Keep track of existing company approvals + approvalList := ApprovalList{ + EmailApprovals: cclaSignature.EmailApprovalList, + DomainApprovals: cclaSignature.DomainApprovalList, + GitHubUsernameApprovals: cclaSignature.GithubUsernameApprovalList, + GitHubOrgApprovals: cclaSignature.GithubOrgApprovalList, + GitlabUsernameApprovals: cclaSignature.GitlabUsernameApprovalList, + GitlabOrgApprovals: cclaSignature.GitlabOrgApprovalList, + CLAManager: claManager, + ICLAs: make([]*models.IclaSignature, 0), + ECLAs: make([]*models.Signature, 0), + ManagersInfo: cclaManagers, + CCLASignature: cclaSignature, + } + + // Just grab and use the first one - need to figure out conflict resolution if more than one + expressionAttributeNames := map[string]*string{} + expressionAttributeValues := map[string]*dynamodb.AttributeValue{} + haveAdditions := false + updateExpression := "" + + employeeSignatureParams := signatures.GetProjectCompanyEmployeeSignaturesParams{ + ProjectID: projectID, + CompanyID: companyID, + PageSize: utils.Int64(10), + } + + // authUser := auth.User{ + // Email: claManager.LfEmail.String(), + // UserName: claManager.LfUsername, + // } + + // Keep track of gerrit users under a give CLA Group + var gerritICLAECLAs []string + + // Only load the gerrit user information, which is costly, if we have updates to remove email or email domains + if (params.RemoveEmailApprovalList != nil && len(params.RemoveEmailApprovalList) > 0) || (params.RemoveDomainApprovalList != nil && len(params.RemoveDomainApprovalList) > 0) { + + goRoutines := 2 + gerritResultChannel := make(chan *GerritUserResponse, goRoutines) + gerritQueryStartTime, _ := utils.CurrentTime() + // go repo.getGerritUsers(ctx, &authUser, projectID, utils.ClaTypeICLA, gerritResultChannel) + // go repo.getGerritUsers(ctx, &authUser, projectID, utils.ClaTypeECLA, gerritResultChannel) + + log.WithFields(f).Debug("waiting on gerrit user query results from 2 go routines...") + for i := 0; i < goRoutines; i++ { + results := <-gerritResultChannel + log.WithFields(f).Debugf("received gerrit user query results response for %s - took: %+v", results.queryType, time.Since(gerritQueryStartTime)) + if results.Error != nil { + log.WithFields(f).WithError(results.Error).Warnf("problem retrieving gerrit users for %s, error: %+v", results.queryType, results.Error) + } else { + for _, member := range results.gerritGroupResponse.Members { + gerritICLAECLAs = append(gerritICLAECLAs, member.Username) + } + log.WithFields(f).Debugf("updated gerrit user query results response for %s - list size is %d...", results.queryType, len(gerritICLAECLAs)) + } + } + log.WithFields(f).Debugf("received the gerrit user query results from %d go routines...", goRoutines) + } + + // If we have an add or remove email list...we need to run an update for this column + if (params.AddEmailApprovalList != nil && len(params.AddEmailApprovalList) > 0) || (params.RemoveEmailApprovalList != nil && len(params.RemoveEmailApprovalList) > 0) { + columnName := SignatureEmailApprovalListColumn + attrList := buildApprovalAttributeList(ctx, cclaSignature.EmailApprovalList, params.AddEmailApprovalList, params.RemoveEmailApprovalList) + // If no entries after consolidating all the updates, we need to remove the column + if attrList == nil || attrList.L == nil { + var rmColErr error + cclaSignature, rmColErr = repo.removeColumn(ctx, cclaSignature.SignatureID, columnName) + if rmColErr != nil { + msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", + columnName, companyID, projectID, true, true) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + } else { + haveAdditions = true + expressionAttributeNames["#E"] = aws.String(columnName) + expressionAttributeValues[":e"] = attrList + updateExpression = updateExpression + " #E = :e, " + } + + log.WithFields(f).Debugf("updating approval list table") + + if params.AddEmailApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddEmailApprovalList, utils.EmailApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, true) + } + + // if email removal update signature approvals + if params.RemoveEmailApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddEmailApprovalList, utils.EmailApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, false) + log.WithFields(f).Debugf("removing email: %+v the approval list", params.RemoveDomainApprovalList) + var wg sync.WaitGroup + wg.Add(len(params.RemoveEmailApprovalList)) + approvalList.Criteria = utils.EmailCriteria + approvalList.ApprovalList = params.RemoveEmailApprovalList + approvalList.Action = utils.RemoveApprovals + approvalList.Version = claGroupModel.Version + for _, email := range params.RemoveEmailApprovalList { + go func(email string) { + defer wg.Done() + var iclas []*models.IclaSignature + var eclas []*models.Signature + log.WithFields(f).Debugf("getting cla user record for email: %s ", email) + userSearch, userErr := repo.usersRepo.SearchUsers("user_emails", email, false) + if userErr != nil || userSearch == nil { + log.WithFields(f).Debugf("error getting user by email: %s ", email) + return + } + criteria := &ApprovalCriteria{ + UserEmail: email, + } + log.WithFields(f).Debugf("Updating signature records for email approval list: %+v ", params.RemoveEmailApprovalList) + signs, appErr := repo.GetProjectCompanyEmployeeSignatures(ctx, employeeSignatureParams, criteria) + if appErr != nil { + log.WithFields(f).Debugf("unable to get Company Employee signatures : %+v ", appErr) + return + } + + if len(signs.Signatures) == 0 { + log.WithFields(f).Debugf("company employee signatures do not exist for company: %s and project: %s ", companyID, projectID) + } + + if len(signs.Signatures) > 0 { + approvalList.ECLAs = signs.Signatures + eclas = signs.Signatures + } + + if len(userSearch.Users) > 0 { + // Try and grab iclaSignature records for users + results := make(chan *ICLAUserResponse, len(userSearch.Users)) + go func() { + defer close(results) + for _, user := range userSearch.Users { + icla, iclaErr := repo.GetIndividualSignature(ctx, projectID, user.UserID, &approved, &signed) + if iclaErr != nil || icla == nil { + results <- &ICLAUserResponse{ + Error: fmt.Errorf("unable to get icla for user: %s ", user.UserID), + } + } else { + + // // Update gerrit user + // if utils.StringInSlice(user.LfUsername, gerritICLAECLAs) { + // // gerritIclaErr := repo.gerritService.RemoveUserFromGroup(ctx, &authUser, approvalList.ClaGroupID, user.LfUsername, utils.ClaTypeICLA) + // if gerritIclaErr != nil { + // msg := fmt.Sprintf("unable to remove gerrit user: %s from group: %s", user.LfUsername, approvalList.ClaGroupID) + // log.WithFields(f).WithError(gerritIclaErr).Warn(msg) + // } + // eclaErr := repo.gerritService.RemoveUserFromGroup(ctx, &authUser, approvalList.ClaGroupID, user.LfUsername, utils.ClaTypeECLA) + // if eclaErr != nil { + // msg := fmt.Sprintf("unable to remove gerrit user: %s from group: %s", user.LfUsername, approvalList.ClaGroupID) + // log.WithFields(f).WithError(eclaErr).Warn(msg) + // } + // } + results <- &ICLAUserResponse{ + ICLASignature: &models.IclaSignature{ + GithubUsername: icla.UserGHUsername, + LfUsername: user.LfUsername, + SignatureID: icla.SignatureID, + }, + } + } + } + }() + + for result := range results { + if result.Error == nil { + log.WithFields(f).Debug("processing icla...") + approvalList.ICLAs = append(approvalList.ICLAs, result.ICLASignature) + iclas = append(iclas, result.ICLASignature) + } + } + + } + + // Invalidate signatures + repo.invalidateSignatures(ctx, &approvalList, claManager, eventArgs) + + // Send email + repo.sendEmail(ctx, email, &approvalList, iclas, eclas) + + }(email) + } + wg.Wait() + } + } + + if (params.AddDomainApprovalList != nil && len(params.AddDomainApprovalList) > 0) || (params.RemoveDomainApprovalList != nil && len(params.RemoveDomainApprovalList) > 0) { + + columnName := SignatureDomainApprovalListColumn + attrList := buildApprovalAttributeList(ctx, cclaSignature.DomainApprovalList, params.AddDomainApprovalList, params.RemoveDomainApprovalList) + // If no entries after consolidating all the updates, we need to remove the column + if attrList == nil || attrList.L == nil { + var rmColErr error + cclaSignature, rmColErr = repo.removeColumn(ctx, cclaSignature.SignatureID, columnName) + if rmColErr != nil { + msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", + columnName, companyID, projectID, true, true) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + } else { + haveAdditions = true + expressionAttributeNames["#D"] = aws.String(columnName) + expressionAttributeValues[":d"] = attrList + updateExpression = updateExpression + " #D = :d, " + } + + log.WithFields(f).Debugf("updating approval list table") + if params.AddDomainApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddDomainApprovalList, utils.EmailApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, true) + } + + if params.RemoveDomainApprovalList != nil { + // Get ICLAs + log.WithFields(f).Debug("getting icla records... ") + iclas, iclaErr := repo.GetClaGroupICLASignatures(ctx, approvalList.ClaGroupID, nil, &approved, &signed, 0, "", true) + if iclaErr != nil { + log.WithFields(f).Warn("unable to get iclas") + } + // Get ECLAs + log.WithFields(f).Debug("getting ecla records... ") + companyProjectParams := signatures.GetProjectCompanyEmployeeSignaturesParams{ + CompanyID: approvalList.CompanyID, + ProjectID: approvalList.ClaGroupID, + PageSize: utils.Int64(10), + } + + criteria := ApprovalCriteria{} + eclas, eclaErr := repo.GetProjectCompanyEmployeeSignatures(ctx, companyProjectParams, &criteria) + if eclaErr != nil { + log.WithFields(f).Warnf("unable to get cclas for company: %s and project: %s ", approvalList.CompanyID, approvalList.ClaGroupID) + } + + approvalList.Criteria = utils.EmailDomainCriteria + approvalList.ApprovalList = params.RemoveDomainApprovalList + approvalList.Action = utils.RemoveApprovals + approvalList.GerritICLAECLAs = gerritICLAECLAs + approvalList.ClaGroupID = projectID + approvalList.ClaGroupName = claGroupModel.ProjectName + approvalList.CompanyID = companyID + approvalList.Version = claGroupModel.Version + if iclas != nil { + approvalList.ICLAs = iclas.List + } + if eclas != nil { + approvalList.ECLAs = eclas.Signatures + } + + repo.invalidateSignatures(ctx, &approvalList, claManager, eventArgs) + repo.updateApprovalTable(ctx, params.AddDomainApprovalList, utils.EmailApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, false) + } + } + + if (params.AddGithubUsernameApprovalList != nil && len(params.AddGithubUsernameApprovalList) > 0) || (params.RemoveGithubUsernameApprovalList != nil && len(params.RemoveGithubUsernameApprovalList) > 0) { + columnName := SignatureGitHubUsernameApprovalListColumn + attrList := buildApprovalAttributeList(ctx, cclaSignature.GithubUsernameApprovalList, params.AddGithubUsernameApprovalList, params.RemoveGithubUsernameApprovalList) + // If no entries after consolidating all the updates, we need to remove the column + if attrList == nil || attrList.L == nil { + var rmColErr error + cclaSignature, rmColErr = repo.removeColumn(ctx, cclaSignature.SignatureID, columnName) + if rmColErr != nil { + msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", + columnName, companyID, projectID, true, true) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + } else { + haveAdditions = true + expressionAttributeNames["#GHU"] = aws.String(columnName) + expressionAttributeValues[":ghu"] = attrList + updateExpression = updateExpression + " #GHU = :ghu, " + } + + if params.AddGithubUsernameApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddGithubUsernameApprovalList, utils.GithubUsernameApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, true) + } + if params.RemoveGithubUsernameApprovalList != nil { + + repo.updateApprovalTable(ctx, params.AddGithubUsernameApprovalList, utils.GithubUsernameApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, false) + // if email removal update signature approvals + if params.RemoveGithubUsernameApprovalList != nil { + var wg sync.WaitGroup + approvalList.Criteria = utils.GitHubUsernameCriteria + approvalList.ApprovalList = params.RemoveGithubUsernameApprovalList + approvalList.Action = utils.RemoveApprovals + approvalList.ClaGroupID = projectID + approvalList.ClaGroupName = claGroupModel.ProjectName + approvalList.CompanyID = companyID + approvalList.Version = claGroupModel.Version + wg.Add(len(params.RemoveGithubUsernameApprovalList)) + for _, ghUsername := range params.RemoveGithubUsernameApprovalList { + go func(ghUsername string) { + defer wg.Done() + var iclas []*models.IclaSignature + var eclas []*models.Signature + + criteria := &ApprovalCriteria{ + GitHubUsername: ghUsername, + } + log.WithFields(f).Debugf("Updating signature records for github username apporval list: %+v ", params.RemoveGithubUsernameApprovalList) + signs, ghUserErr := repo.GetProjectCompanyEmployeeSignatures(ctx, employeeSignatureParams, criteria) + if ghUserErr != nil { + log.WithFields(f).Debugf("unable to get Company Employee signatures : %+v ", ghUserErr) + return + } + if signs.Signatures != nil { + approvalList.ECLAs = signs.Signatures + eclas = signs.Signatures + } + // Get ICLAs + claUser, claErr := repo.usersRepo.GetUserByGitHubUsername(ghUsername) + if claErr != nil { + log.WithFields(f).Debugf("unable to get user by github username: %s ", ghUsername) + return + } + if claUser != nil { + icla, iclaErr := repo.GetIndividualSignature(ctx, projectID, claUser.UserID, &approved, &signed) + if iclaErr != nil || icla == nil { + log.WithFields(f).Debugf("unable to get icla signature for user with github username: %s ", ghUsername) + } + if icla != nil { + // Convert to IclSignature instance to leverage invalidateSignatures helper function + approvalList.ICLAs = []*models.IclaSignature{{ + GithubUsername: icla.UserGHUsername, + LfUsername: icla.UserLFID, + SignatureID: icla.SignatureID, + }} + } + } + + repo.invalidateSignatures(ctx, &approvalList, claManager, eventArgs) + + // Send Email + repo.sendEmail(ctx, getBestEmail(claUser), &approvalList, iclas, eclas) + + }(ghUsername) + } + wg.Wait() + } + } } - // Just grab and use the first one - need to figure out conflict resolution if more than one - sig := sigs.Signatures[0] - expressionAttributeNames := map[string]*string{} - expressionAttributeValues := map[string]*dynamodb.AttributeValue{} - haveAdditions := false - updateExpression := "" - - // If we have an add or remove email list...we need to run an update for this column - if params.AddEmailApprovalList != nil || params.RemoveEmailApprovalList != nil { - columnName := "email_whitelist" - attrList := buildApprovalAttributeList(ctx, sig.EmailApprovalList, params.AddEmailApprovalList, params.RemoveEmailApprovalList) + if (params.AddGithubOrgApprovalList != nil && len(params.AddGithubOrgApprovalList) > 0) || (params.RemoveGithubOrgApprovalList != nil && len(params.RemoveGithubOrgApprovalList) > 0) { + columnName := SignatureGitHubOrgApprovalListColumn + attrList := buildApprovalAttributeList(ctx, cclaSignature.GithubOrgApprovalList, params.AddGithubOrgApprovalList, params.RemoveGithubOrgApprovalList) // If no entries after consolidating all the updates, we need to remove the column if attrList == nil || attrList.L == nil { var rmColErr error - sig, rmColErr = repo.removeColumn(ctx, sig.SignatureID.String(), columnName) + cclaSignature, rmColErr = repo.removeColumn(ctx, cclaSignature.SignatureID, columnName) if rmColErr != nil { msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - columnName, companyID, projectID, signed, approved) + columnName, companyID, projectID, true, true) log.WithFields(f).Warn(msg) return nil, errors.New(msg) } } else { haveAdditions = true - expressionAttributeNames["#E"] = aws.String("email_whitelist") - expressionAttributeValues[":e"] = attrList - updateExpression = updateExpression + " #E = :e, " + expressionAttributeNames["#GHO"] = aws.String(columnName) + expressionAttributeValues[":gho"] = attrList + updateExpression = updateExpression + " #GHO = :gho, " } - } - if params.AddDomainApprovalList != nil || params.RemoveDomainApprovalList != nil { - columnName := "domain_whitelist" - attrList := buildApprovalAttributeList(ctx, sig.DomainApprovalList, params.AddDomainApprovalList, params.RemoveDomainApprovalList) - // If no entries after consolidating all the updates, we need to remove the column - if attrList == nil || attrList.L == nil { - var rmColErr error - sig, rmColErr = repo.removeColumn(ctx, sig.SignatureID.String(), columnName) - if rmColErr != nil { - msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - columnName, companyID, projectID, signed, approved) - log.WithFields(f).Warn(msg) + if params.AddGithubOrgApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddGithubOrgApprovalList, utils.GithubOrgApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, true) + } + + if params.RemoveGithubOrgApprovalList != nil { + approvalList.Criteria = utils.GitHubOrgCriteria + approvalList.ApprovalList = params.RemoveGithubOrgApprovalList + approvalList.Action = utils.RemoveApprovals + approvalList.Version = claGroupModel.Version + // Get repositories by CLAGroup + repositories, getRepoByCLAGroupErr := repo.repositoriesRepo.GitHubGetRepositoriesByCLAGroup(ctx, projectID, true) + if getRepoByCLAGroupErr != nil { + msg := fmt.Sprintf("unable to fetch repositories for cla group ID: %s ", projectID) + log.WithFields(f).WithError(getRepoByCLAGroupErr).Warn(msg) return nil, errors.New(msg) } - } else { - haveAdditions = true - expressionAttributeNames["#D"] = aws.String(columnName) - expressionAttributeValues[":d"] = attrList - updateExpression = updateExpression + " #D = :d, " + var ghOrgRepositories []*models.GithubRepository + var ghOrgs []*models.GithubOrganization + for _, repository := range repositories { + // Check for matching organization name in repositories table against approvalList removal GitHub organizations + if utils.StringInSlice(repository.RepositoryOrganizationName, approvalList.ApprovalList) { + ghOrgRepositories = append(ghOrgRepositories, repository) + } + } + + for _, ghOrgRepo := range ghOrgRepositories { + ghOrg, getGHOrgErr := repo.ghOrgRepo.GetGitHubOrganization(ctx, ghOrgRepo.RepositoryOrganizationName) + if getGHOrgErr != nil { + msg := fmt.Sprintf("unable to get gh org by name: %s ", ghOrgRepo.RepositoryOrganizationName) + log.WithFields(f).WithError(getGHOrgErr).Warn(msg) + return nil, errors.New(msg) + } + ghOrgs = append(ghOrgs, ghOrg) + } + + var ghUsernames []string + for _, ghOrg := range ghOrgs { + ghOrgUsers, getOrgMembersErr := github.GetOrganizationMembers(ctx, ghOrg.OrganizationName, ghOrg.OrganizationInstallationID) + if getOrgMembersErr != nil { + msg := fmt.Sprintf("unable to fetch github organization users for org: %s ", ghOrg.OrganizationName) + log.WithFields(f).WithError(getOrgMembersErr).Warnf(msg) + return nil, errors.New(msg) + } + ghUsernames = append(ghUsernames, ghOrgUsers...) + } + approvalList.GitHubUsernames = utils.RemoveDuplicates(ghUsernames) + + repo.invalidateSignatures(ctx, &approvalList, claManager, eventArgs) + repo.updateApprovalTable(ctx, params.AddGithubOrgApprovalList, utils.GithubOrgApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, false) } } - if params.AddGithubUsernameApprovalList != nil || params.RemoveGithubUsernameApprovalList != nil { - columnName := "github_whitelist" - attrList := buildApprovalAttributeList(ctx, sig.GithubUsernameApprovalList, params.AddGithubUsernameApprovalList, params.RemoveGithubUsernameApprovalList) + if (params.AddGitlabUsernameApprovalList != nil && len(params.AddGitlabUsernameApprovalList) > 0) || (params.RemoveGitlabUsernameApprovalList != nil && len(params.RemoveGitlabUsernameApprovalList) > 0) { + columnName := SignatureGitlabUsernameApprovalListColumn + attrList := buildApprovalAttributeList(ctx, cclaSignature.GitlabUsernameApprovalList, params.AddGitlabUsernameApprovalList, params.RemoveGitlabUsernameApprovalList) // If no entries after consolidating all the updates, we need to remove the column if attrList == nil || attrList.L == nil { var rmColErr error - sig, rmColErr = repo.removeColumn(ctx, sig.SignatureID.String(), columnName) + cclaSignature, rmColErr = repo.removeColumn(ctx, cclaSignature.SignatureID, columnName) if rmColErr != nil { msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - columnName, companyID, projectID, signed, approved) + columnName, companyID, projectID, true, true) log.WithFields(f).Warn(msg) return nil, errors.New(msg) } } else { haveAdditions = true - expressionAttributeNames["#G"] = aws.String(columnName) - expressionAttributeValues[":g"] = attrList - updateExpression = updateExpression + " #G = :g, " + expressionAttributeNames["#GLU"] = aws.String(columnName) + expressionAttributeValues[":glu"] = attrList + updateExpression = updateExpression + " #GLU = :glu, " + } + if params.AddGitlabUsernameApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddGitlabUsernameApprovalList, utils.GitlabUsernameApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, true) + } + if params.RemoveGitlabUsernameApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddGitlabUsernameApprovalList, utils.GitlabUsernameApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, false) + // if email removal update signature approvals + if params.RemoveGitlabUsernameApprovalList != nil { + approvalList.Criteria = utils.GitlabUsernameCriteria + approvalList.ApprovalList = params.RemoveGitlabUsernameApprovalList + approvalList.Action = utils.RemoveApprovals + approvalList.ClaGroupID = projectID + approvalList.ClaGroupName = claGroupModel.ProjectName + approvalList.CompanyID = companyID + approvalList.Version = claGroupModel.Version + + // Get ICLAs + var wg sync.WaitGroup + wg.Add(len(params.RemoveGitlabUsernameApprovalList)) + for _, ghUsername := range params.RemoveGitlabUsernameApprovalList { + go func(gitLabUsername string) { + defer wg.Done() + var iclas []*models.IclaSignature + var eclas []*models.Signature + + criteria := &ApprovalCriteria{ + GitlabUsername: gitLabUsername, + } + log.WithFields(f).Debugf("Updating signature records for gitlab username apporval list: %+v ", params.RemoveGitlabUsernameApprovalList) + signs, ghUserErr := repo.GetProjectCompanyEmployeeSignatures(ctx, employeeSignatureParams, criteria) + if ghUserErr != nil { + log.WithFields(f).Debugf("unable to get Company Employee signatures : %+v ", ghUserErr) + return + } + if signs.Signatures != nil { + approvalList.ECLAs = signs.Signatures + eclas = signs.Signatures + } + + claUser, claErr := repo.usersRepo.GetUserByGitLabUsername(gitLabUsername) + if claErr != nil { + log.WithFields(f).Debugf("unable to get User by gitlab username: %s ", gitLabUsername) + return + } + if claUser != nil { + icla, iclaErr := repo.GetIndividualSignature(ctx, projectID, claUser.UserID, &approved, &signed) + if iclaErr != nil || icla == nil { + log.WithFields(f).Debugf("unable to get icla signature for user with gitlab username: %s ", gitLabUsername) + } + if icla != nil { + // Convert to IclSignature instance to leverage invalidateSignatures helper function + approvalList.ICLAs = []*models.IclaSignature{{ + GitlabUsername: icla.UserGHUsername, + LfUsername: icla.UserLFID, + SignatureID: icla.SignatureID, + }} + } + } + + repo.invalidateSignatures(ctx, &approvalList, claManager, eventArgs) + + // Send Email + repo.sendEmail(ctx, getBestEmail(claUser), &approvalList, iclas, eclas) + + }(ghUsername) + } + wg.Wait() + } } } - if params.AddGithubOrgApprovalList != nil || params.RemoveGithubOrgApprovalList != nil { - columnName := "github_org_whitelist" - attrList := buildApprovalAttributeList(ctx, sig.GithubOrgApprovalList, params.AddGithubOrgApprovalList, params.RemoveGithubOrgApprovalList) + if (params.AddGitlabOrgApprovalList != nil && len(params.AddGitlabOrgApprovalList) > 0) || (params.RemoveGitlabOrgApprovalList != nil && len(params.RemoveGitlabOrgApprovalList) > 0) { + columnName := SignatureGitlabOrgApprovalListColumn + attrList := buildApprovalAttributeList(ctx, cclaSignature.GitlabOrgApprovalList, params.AddGitlabOrgApprovalList, params.RemoveGitlabOrgApprovalList) // If no entries after consolidating all the updates, we need to remove the column if attrList == nil || attrList.L == nil { var rmColErr error - sig, rmColErr = repo.removeColumn(ctx, sig.SignatureID.String(), columnName) + cclaSignature, rmColErr = repo.removeColumn(ctx, cclaSignature.SignatureID, columnName) if rmColErr != nil { msg := fmt.Sprintf("unable to remove column %s for signature for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - columnName, companyID, projectID, signed, approved) + columnName, companyID, projectID, true, true) log.WithFields(f).Warn(msg) return nil, errors.New(msg) } } else { haveAdditions = true - expressionAttributeNames["#GO"] = aws.String("github_org_whitelist") - expressionAttributeValues[":go"] = attrList - updateExpression = updateExpression + " #GO = :go, " + expressionAttributeNames["#GLO"] = aws.String(columnName) + expressionAttributeValues[":glo"] = attrList + updateExpression = updateExpression + " #GLO = :glo, " } - } + if params.AddGitlabOrgApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddGitlabOrgApprovalList, utils.GitlabOrgApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, true) + } + + if params.RemoveGitlabOrgApprovalList != nil { + repo.updateApprovalTable(ctx, params.AddGitlabOrgApprovalList, utils.GitlabOrgApprovalCriteria, signatureID, projectID, companyID, cclaSignature.SignatureReferenceName, false) + approvalList.Criteria = utils.GitlabOrgCriteria + approvalList.ApprovalList = params.RemoveGitlabOrgApprovalList + approvalList.Action = utils.RemoveApprovals + approvalList.Version = claGroupModel.Version + // Get repositories by CLAGroup + repositories, getRepoByCLAGroupErr := repo.repositoriesRepo.GitHubGetRepositoriesByCLAGroup(ctx, projectID, true) + if getRepoByCLAGroupErr != nil { + msg := fmt.Sprintf("unable to fetch repositories for cla group ID: %s ", projectID) + log.WithFields(f).WithError(getRepoByCLAGroupErr).Warn(msg) + return nil, errors.New(msg) + } + var gitLabOrgRepositories []*models.GithubRepository + var gitLabOrgs []*models.GithubOrganization + for _, repository := range repositories { + // Check for matching organization name in repositories table against approvalList removal gitlab organizations/groups + if utils.StringInSlice(repository.RepositoryOrganizationName, approvalList.ApprovalList) { + gitLabOrgRepositories = append(gitLabOrgRepositories, repository) + } + } + + for _, gitLabOrgRepo := range gitLabOrgRepositories { + gitLabOrg, getGitlabOrgErr := repo.ghOrgRepo.GetGitHubOrganization(ctx, gitLabOrgRepo.RepositoryOrganizationName) + if getGitlabOrgErr != nil { + msg := fmt.Sprintf("unable to get gitlab organization by name: %s ", gitLabOrgRepo.RepositoryOrganizationName) + log.WithFields(f).WithError(getGitlabOrgErr).Warn(msg) + return nil, errors.New(msg) + } + gitLabOrgs = append(gitLabOrgs, gitLabOrg) + } + + var gitLabUsernames []string + for _, gitLabOrg := range gitLabOrgs { + gitLabOrgUsers, getOrgMembersErr := github.GetOrganizationMembers(ctx, gitLabOrg.OrganizationName, gitLabOrg.OrganizationInstallationID) + if getOrgMembersErr != nil { + msg := fmt.Sprintf("unable to fetch gitLabOrgUsers for org: %s ", gitLabOrg.OrganizationName) + log.WithFields(f).WithError(getOrgMembersErr).Warnf(msg) + return nil, errors.New(msg) + } + gitLabUsernames = append(gitLabUsernames, gitLabOrgUsers...) + } + approvalList.GitlabUsernames = utils.RemoveDuplicates(gitLabUsernames) + + repo.invalidateSignatures(ctx, &approvalList, claManager, eventArgs) + } + } // Ensure at least one value is set for us to update if !haveAdditions { log.WithFields(f).Debugf("no updates required to any of the approved list values company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t - expecting at least something to update", - companyID, projectID, signed, approved) - return sig, nil + companyID, projectID, true, true) + return cclaSignature, nil } // Remove trailing comma from the expression, if present @@ -1854,17 +3761,14 @@ func (repo repository) UpdateApprovalList(ctx context.Context, projectID, compan TableName: aws.String(repo.signatureTableName), Key: map[string]*dynamodb.AttributeValue{ "signature_id": { - S: aws.String(sig.SignatureID.String()), + S: aws.String(cclaSignature.SignatureID), }, }, ExpressionAttributeNames: expressionAttributeNames, ExpressionAttributeValues: expressionAttributeValues, - UpdateExpression: aws.String(updateExpression), //aws.String("SET #L = :l"), + UpdateExpression: aws.String(updateExpression), } - log.WithFields(f).Debugf("updating approval list for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - companyID, projectID, signed, approved) - _, updateErr := repo.dynamoDBClient.UpdateItem(input) if updateErr != nil { log.WithFields(f).Warnf("error updating approval lists for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t, error: %v", @@ -1872,34 +3776,348 @@ func (repo repository) UpdateApprovalList(ctx context.Context, projectID, compan return nil, updateErr } - log.WithFields(f).Debugf("querying database for approval list details after update using company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - companyID, projectID, signed, approved) + // Query the CCLA signature once again to load the most recent updates which include approval list updates from above + updatedSig, err := repo.GetCorporateSignature(ctx, projectID, companyID, &approved, &signed) + if err != nil || cclaSignature == nil { + msg := fmt.Sprintf("unable to get corporate signature for CLA Group: %s and company: %s", projectID, companyID) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } - updatedSig, sigErr := repo.GetProjectCompanySignatures(ctx, companyID, projectID, &signed, &approved, nil, &sortOrder, &pageSize) - if sigErr != nil { - return nil, sigErr + // Just grab and use the first one - need to figure out conflict resolution if more than one + return updatedSig, nil +} + +func (repo *repository) updateApprovalTable(ctx context.Context, approvalList []string, criteria, signatureID, projectID, companyID, companyName string, add bool) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.addApprovalList", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), } - if updatedSig == nil || updatedSig.Signatures == nil { - msg := fmt.Sprintf("unable to locate signature after update for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t", - companyID, projectID, signed, approved) - log.WithFields(f).Warn(msg) - return nil, errors.New(msg) + for _, item := range approvalList { + log.WithFields(f).Debugf("adding approval request for item: %s with criteria: %s", item, criteria) + approvalID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to generate UUID for email: %s", item) + continue + } + _, currentTime := utils.CurrentTime() + + // Check if it exists + approvalItems, apprErr := repo.approvalRepo.SearchApprovalList(criteria, item, projectID, companyID, signatureID) + if apprErr != nil { + log.WithFields(f).WithError(apprErr).Warnf("unable to search approval list for item: %s", item) + continue + } + + if len(approvalItems) > 0 { + // Update the existing record + approvalItem := approvalItems[0] + if add { + log.WithFields(f).Debugf("approval request for item: %s with criteria: %s already exists", item, criteria) + approvalItem.DateModified = currentTime + approvalItem.DateAdded = currentTime + approvalItem.Active = true + } else { + log.WithFields(f).Debugf("approval request for item: %s with criteria: %s already exists", item, criteria) + approvalItem.DateModified = currentTime + approvalItem.DateRemoved = currentTime + approvalItem.Active = false + } + err = repo.approvalRepo.UpdateApprovalItem(approvalItem) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update approval request for item: %s", item) + continue + } + log.WithFields(f).Debugf("updated approval request for item: %s with criteria: %s", item, criteria) + continue + } + + // create a new record + approvalItem := approvals.ApprovalItem{ + ApprovalID: approvalID.String(), + SignatureID: signatureID, + ApprovalName: item, + ProjectID: projectID, + CompanyID: companyID, + ApprovalCriteria: criteria, + DateCreated: currentTime, + DateModified: currentTime, + ApprovalCompanyName: companyName, + } + + if add { + approvalItem.Active = true + approvalItem.DateAdded = currentTime + approvalItem.Note = "Auto-Added" + } else { + approvalItem.Active = false + approvalItem.DateRemoved = currentTime + approvalItem.Note = "Auto-Removed" + } + + err = repo.approvalRepo.AddApprovalList(approvalItem) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to add approval request for item: %s", item) + continue + } + log.WithFields(f).Debugf("added approval request for item: %s with criteria: %s", item, criteria) } +} - if len(updatedSig.Signatures) > 1 { - log.WithFields(f).Warnf("more than 1 CCLA signature returned after update for company ID: %s project ID: %s, type: ccla, signed: %t, approved: %t - expecting zero or 1 - using first record", - companyID, projectID, signed, approved) +// sendEmail is a helper function used to render email for (CCLA, ICLA, ECLA cases) +func (repo repository) sendEmail(ctx context.Context, email string, approvalList *ApprovalList, iclas []*models.IclaSignature, eclas []*models.Signature) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.sendEmail", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + companyName := "" + company, companyErr := repo.companyRepo.GetCompany(ctx, approvalList.CompanyID) + if companyErr != nil { + log.WithFields(f).Debugf("unable to get company") + } + if company != nil { + companyName = company.CompanyName } - // Just grab and use the first one - need to figure out conflict resolution if more than one - return updatedSig.Signatures[0], nil + params := InvalidateSignatureTemplateParams{ + Company: companyName, + RecipientName: email, + ClaManager: utils.GetBestUsername(approvalList.CLAManager), + CLAManagers: approvalList.ManagersInfo, + CLAGroupName: approvalList.ClaGroupName, + } + + // check for signature type (CCLA, ICLA, ECLA) + var removalType = "" + + // case 1 CCLA + if len(iclas) == 0 && len(eclas) == 0 { + removalType = CCLA + } else if len(iclas) > 0 && len(eclas) == 0 { + // case 2 ccla + icla + removalType = CCLAICLA + } else if len(iclas) > 0 && len(eclas) > 0 { + // case 3 ccla + icla + ecla + removalType = CCLAICLAECLA + } + + // Send CCLA Email + if removalType == CCLA { + subject := fmt.Sprintf("EasyCLA: CCLA invalidated for :%s ", approvalList.ClaGroupName) + log.WithFields(f).Debugf("sending ccla invalidation email to :%s ", email) + body, renderErr := utils.RenderTemplate(approvalList.Version, InvalidateCCLASignatureTemplateName, InvalidateCCLASignatureTemplate, 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 { + log.WithFields(f).Debugf("unable to send approval list update email to : %s ", email) + } + } + } else if removalType == ICLA { + subject := fmt.Sprintf("EasyCLA: ICLA invalidated for :%s ", approvalList.ClaGroupName) + log.WithFields(f).Debugf("sending icla invalidation email to :%s ", email) + body, renderErr := utils.RenderTemplate(approvalList.Version, InvalidateICLASignatureTemplateName, 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 { + log.WithFields(f).Debugf("unable to send approval list update email to : %s ", email) + } + } + } else if removalType == CCLAICLA { + subject := fmt.Sprintf("EasyCLA: ICLA invalidated for :%s ", approvalList.ClaGroupName) + log.WithFields(f).Debugf("sending icla invalidation email to :%s ", email) + body, renderErr := utils.RenderTemplate(approvalList.Version, InvalidateCCLAICLASignatureTemplateName, InvalidateCCLASignatureTemplate, 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 { + log.WithFields(f).Debugf("unable to send approval list update email to : %s ", email) + } + } + } else if removalType == CCLAICLAECLA { + subject := fmt.Sprintf("EasyCLA: Employee Acknowledgement invalidated for :%s ", approvalList.ClaGroupName) + log.WithFields(f).Debugf("sending employee acknowledgement invalidation email to :%s ", email) + body, renderErr := utils.RenderTemplate(approvalList.Version, InvalidateCCLAICLAECLASignatureTemplateName, InvalidateCCLAICLAECLASignatureTemplate, 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 { + log.WithFields(f).Debugf("unable to send approval list update email to : %s ", email) + } + } + } +} + +// invalidateSignatures is a helper function that invalidates signature records based on approval list +func (repo repository) invalidateSignatures(ctx context.Context, approvalList *ApprovalList, claManager *models.User, eventArgs *events.LogEventArgs) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.invalidateSignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": &approvalList, + } + + if approvalList.ICLAs != nil { + var iclaWg sync.WaitGroup + //Iterate iclas + iclaWg.Add(len(approvalList.ICLAs)) + log.WithFields(f).Debug("invalidating signature icla records... ") + for _, icla := range approvalList.ICLAs { + go func(icla *models.IclaSignature) { + defer iclaWg.Done() + signature, sigErr := repo.GetSignature(ctx, icla.SignatureID) + if sigErr != nil { + log.WithFields(f).Warnf("unable to fetch signature for ID: %s ", icla.SignatureID) + return + } + // Grab user record + if signature.SignatureReferenceID == "" { + log.WithFields(f).Warnf("no signatureReferenceID for signature: %+v ", signature) + return + } + + user, verifyErr := repo.verifyUserApprovals(ctx, signature.SignatureReferenceID, signature.SignatureID, claManager, approvalList) + if verifyErr != nil { + log.WithFields(f).Warnf("unable to verify user: %s ", signature.SignatureReferenceID) + return + } + // Map representing CLA types against email .... + email := getBestEmail(user) + // Log Event + eventArgs.EventData = &events.SignatureInvalidatedApprovalRejectionEventData{ + SignatureID: icla.SignatureID, + CLAManager: claManager, + CLAGroupID: signature.ProjectID, + Email: email, + GHUsername: user.GithubUsername, + } + repo.eventsService.LogEventWithContext(ctx, eventArgs) + }(icla) + } + iclaWg.Wait() + } + + if approvalList.ECLAs != nil { + var eclaWg sync.WaitGroup + log.WithFields(f).Debug("invalidating signature ecla records... ") + // Iterate eclas + eclaWg.Add(len(approvalList.ECLAs)) + for _, ecla := range approvalList.ECLAs { + go func(ecla *models.Signature) { + defer eclaWg.Done() + // Grab user record + if ecla.SignatureReferenceID == "" { + log.WithFields(f).Warnf("no signatureReferenceID for signature: %+v ", ecla) + return + } + user, verifyErr := repo.verifyUserApprovals(ctx, ecla.SignatureReferenceID, ecla.SignatureID, claManager, approvalList) + if verifyErr != nil { + log.WithFields(f).Warnf("unable to verify user: %s ", ecla.SignatureReferenceID) + return + } + email := getBestEmail(user) + // Log Event + eventArgs.EventData = &events.SignatureInvalidatedApprovalRejectionEventData{ + SignatureID: ecla.SignatureID, + CLAManager: claManager, + CLAGroupID: ecla.ProjectID, + Email: email, + GHUsername: user.GithubUsername, + } + repo.eventsService.LogEventWithContext(ctx, eventArgs) + }(ecla) + } + eclaWg.Wait() + } +} + +// verify UserApprovals checks user +func (repo repository) verifyUserApprovals(ctx context.Context, userID, signatureID string, claManager *models.User, approvalList *ApprovalList) (*models.User, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.verifyUserApprovals", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "userID": userID, + } + + user, err := repo.usersRepo.GetUser(userID) + if err != nil { + log.WithFields(f).Warnf("unable to get user record for ID: %s ", userID) + return nil, err + } + email := getBestEmail(user) + + // authUser := auth.User{ + // Email: claManager.LfEmail.String(), + // UserName: claManager.LfUsername, + // } + + if approvalList.Criteria == utils.EmailDomainCriteria { + // Handle Domains + log.WithFields(f).Debugf("Handling domain for user email: %s with approval list: %+v ", email, approvalList.ApprovalList) + domain := strings.Split(email, "@")[1] + if utils.StringInSlice(domain, approvalList.ApprovalList) { + if (!utils.StringInSlice(user.GithubUsername, approvalList.GitHubUsernameApprovals) || utils.StringInSlice(user.LfUsername, approvalList.GerritICLAECLAs)) && !utils.StringInSlice(email, approvalList.EmailApprovals) { + //Invalidate record + note := fmt.Sprintf("Signature invalidated (approved set to false) by %s due to %s removal", utils.GetBestUsername(claManager), utils.EmailDomainCriteria) + err := repo.InvalidateProjectRecord(ctx, signatureID, note) + if err != nil { + log.WithFields(f).Warnf("unable to invalidate record for signatureID: %s ", signatureID) + return user, err + } + + // // Update Gerrit group users + // if utils.StringInSlice(user.LfUsername, approvalList.GerritICLAECLAs) { + // log.WithFields(f).Debugf("removing gerrit user:%s from claGroup: %s ...", user.LfUsername, approvalList.ClaGroupID) + // iclaErr := repo.gerritService.RemoveUserFromGroup(ctx, &authUser, approvalList.ClaGroupID, user.LfUsername, utils.ClaTypeICLA) + // if iclaErr != nil { + // msg := fmt.Sprintf("unable to remove gerrit user:%s from group:%s", user.LfUsername, approvalList.ClaGroupID) + // log.WithFields(f).Warn(msg) + // } + // eclaErr := repo.gerritService.RemoveUserFromGroup(ctx, &authUser, approvalList.ClaGroupID, user.LfUsername, utils.ClaTypeECLA) + // if eclaErr != nil { + // msg := fmt.Sprintf("unable to remove gerrit user:%s from group:%s", user.LfUsername, approvalList.ClaGroupID) + // log.WithFields(f).Warn(msg) + // } + // } + } + } + } else if approvalList.Criteria == utils.GitHubOrgCriteria { + // Handle GH Org Approvals + if utils.StringInSlice(user.GithubUsername, approvalList.GitHubUsernames) { + if !utils.StringInSlice(getBestEmail(user), approvalList.EmailApprovals) && !utils.StringInSlice(user.GithubUsername, approvalList.GitHubUsernameApprovals) { + //Invalidate record + + note := fmt.Sprintf("Signature invalidated (approved set to false) by %s due to %s removal", utils.GetBestUsername(claManager), utils.GitHubOrgCriteria) + err := repo.InvalidateProjectRecord(ctx, signatureID, note) + if err != nil { + log.WithFields(f).Warnf("unable to invalidate record for signatureID: %s ", signatureID) + return user, err + } + } + } + } else if approvalList.Criteria == utils.GitHubUsernameCriteria || approvalList.Criteria == utils.EmailCriteria { + note := fmt.Sprintf("Signature invalidated (approved set to false) by %s due to %s removal", utils.GetBestUsername(claManager), approvalList.Criteria) + err := repo.InvalidateProjectRecord(ctx, signatureID, note) + if err != nil { + log.WithFields(f).Warnf("unable to invalidate record for signatureID: %s ", signatureID) + return user, err + } + + } + + return user, nil } // removeColumn is a helper function to remove a given column when we need to zero out the column value - typically the approval list func (repo repository) removeColumn(ctx context.Context, signatureID, columnName string) (*models.Signature, error) { f := logrus.Fields{ - "functionName": "removeColumn", + "functionName": "v1.signatures.repository.removeColumn", "signatureID": signatureID, "columnName": columnName, } @@ -1916,12 +4134,7 @@ func (repo repository) removeColumn(ctx context.Context, signatureID, columnName ExpressionAttributeNames: map[string]*string{ "#" + columnName: aws.String(columnName), }, - //ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ - // ":a": { - // S: aws.String("bar"), - // }, - //}, - UpdateExpression: aws.String("REMOVE #" + columnName), //aws.String("REMOVE github_org_whitelist"), + UpdateExpression: aws.String("REMOVE #" + columnName), ReturnValues: aws.String(dynamodb.ReturnValueNone), } @@ -1941,7 +4154,7 @@ func (repo repository) removeColumn(ctx context.Context, signatureID, columnName func (repo repository) AddSigTypeSignedApprovedID(ctx context.Context, signatureID string, val string) error { f := logrus.Fields{ - "functionName": "AddSigTypeSignedApprovedID", + "functionName": "v1.signatures.repository.AddSigTypeSignedApprovedID", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, "sigtypeSignedApprovedID": val, @@ -1973,7 +4186,7 @@ func (repo repository) AddSigTypeSignedApprovedID(ctx context.Context, signature } func (repo repository) AddUsersDetails(ctx context.Context, signatureID string, userID string) error { f := logrus.Fields{ - "functionName": "AddUserDetails", + "functionName": "v1.signatures.repository.AddUserDetails", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, "userID": userID, @@ -1988,7 +4201,7 @@ func (repo repository) AddUsersDetails(ctx context.Context, signatureID string, } var email string if userModel.LfEmail != "" { - email = userModel.LfEmail + email = userModel.LfEmail.String() } else { if len(userModel.Emails) > 0 { email = userModel.Emails[0] @@ -2004,7 +4217,7 @@ func (repo repository) AddUsersDetails(ctx context.Context, signatureID string, }, } ue := utils.NewDynamoUpdateExpression() - ue.AddAttributeName("#gh_username", "user_github_username", userModel.GithubUsername != "") + ue.AddAttributeName("#gh_username", SignatureUserGitHubUsername, userModel.GithubUsername != "") ue.AddAttributeName("#lf_username", "user_lf_username", userModel.LfUsername != "") ue.AddAttributeName("#name", "user_name", userModel.Username != "") ue.AddAttributeName("#email", "user_email", email != "") @@ -2038,7 +4251,7 @@ func (repo repository) AddUsersDetails(ctx context.Context, signatureID string, func (repo repository) AddSignedOn(ctx context.Context, signatureID string) error { f := logrus.Fields{ - "functionName": "AddSignedOn", + "functionName": "v1.signatures.repository.AddSignedOn", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "signatureID": signatureID, } @@ -2073,362 +4286,386 @@ func (repo repository) AddSignedOn(ctx context.Context, signatureID string) erro return nil } -// buildProjectSignatureModels converts the response model into a response data model -func (repo repository) buildProjectSignatureModels(ctx context.Context, results *dynamodb.QueryOutput, projectID string, loadACLDetails bool) ([]*models.Signature, error) { +func (repo repository) GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) { f := logrus.Fields{ - "functionName": "buildProjectSignatureModels", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "projectID": projectID, + "functionName": "v1.signatures.repository.GetClaGroupICLASignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "searchTerm": utils.StringValue(searchTerm), + "withExtraDetails": withExtraDetails, } - var sigs []*models.Signature - // The DB signature model - var dbSignatures []ItemSignature + condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)) - err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) - if err != nil { - log.WithFields(f).Warnf("error unmarshalling signatures from database for project: %s, error: %v", - projectID, err) - return nil, err + var filter expression.ConditionBuilder + var filterAdded bool + filter = addAndCondition(filter, expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_reference_type").Equal(expression.Value(utils.SignatureReferenceTypeUser)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").AttributeNotExists(), &filterAdded) + + if approved != nil { + f["approved"] = utils.BoolValue(approved) + //log.WithFields(f).Debugf("adding filter signature_approved: %t", aws.BoolValue(approved)) + searchTermExpression := expression.Name("signature_approved").Equal(expression.Value(aws.BoolValue(approved))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } + if signed != nil { + f["signed"] = utils.BoolValue(signed) + //log.WithFields(f).Debugf("adding filter signature_signed: %t", aws.BoolValue(signed)) + searchTermExpression := expression.Name("signature_signed").Equal(expression.Value(aws.BoolValue(signed))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) } - var wg sync.WaitGroup - wg.Add(len(dbSignatures)) - for _, dbSignature := range dbSignatures { - - // Set the signature type in the response - var claType = "" - // Corporate Signature - if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeCompany && dbSignature.SignatureType == utils.SignatureTypeCCLA { - claType = utils.ClaTypeCCLA - } - // Employee Signature - if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeUser && dbSignature.SignatureType == utils.SignatureTypeCLA && dbSignature.SignatureUserCompanyID != "" { - claType = utils.ClaTypeECLA - } - - // Individual Signature - if dbSignature.SignatureReferenceType == utils.SignatureReferenceTypeUser && dbSignature.SignatureType == utils.SignatureTypeCLA && dbSignature.SignatureUserCompanyID == "" { - claType = utils.ClaTypeICLA - } - - sig := &models.Signature{ - SignatureID: strfmt.UUID4(dbSignature.SignatureID), - ClaType: claType, - SignatureCreated: dbSignature.DateCreated, - SignatureModified: dbSignature.DateModified, - SignatureType: dbSignature.SignatureType, - SignatureReferenceID: strfmt.UUID4(dbSignature.SignatureReferenceID), - SignatureReferenceName: dbSignature.SignatureReferenceName, - SignatureReferenceNameLower: dbSignature.SignatureReferenceNameLower, - SignatureSigned: dbSignature.SignatureSigned, - SignatureApproved: dbSignature.SignatureApproved, - SignatureMajorVersion: dbSignature.SignatureDocumentMajorVersion, - SignatureMinorVersion: dbSignature.SignatureDocumentMinorVersion, - Version: dbSignature.SignatureDocumentMajorVersion + "." + dbSignature.SignatureDocumentMinorVersion, - SignatureReferenceType: dbSignature.SignatureReferenceType, - ProjectID: dbSignature.SignatureProjectID, - Created: dbSignature.DateCreated, - Modified: dbSignature.DateModified, - EmailApprovalList: dbSignature.EmailWhitelist, - DomainApprovalList: dbSignature.DomainWhitelist, - GithubUsernameApprovalList: dbSignature.GitHubWhitelist, - GithubOrgApprovalList: dbSignature.GitHubOrgWhitelist, - UserName: dbSignature.UserName, - UserLFID: dbSignature.UserLFUsername, - UserGHID: dbSignature.UserGithubUsername, - SignedOn: dbSignature.SignedOn, - SignatoryName: dbSignature.SignatoryName, - UserDocusignName: dbSignature.UserDocusignName, - UserDocusignDateSigned: dbSignature.UserDocusignDateSigned, - } - sigs = append(sigs, sig) - go func(sigModel *models.Signature, signatureUserCompanyID string, sigACL []string) { - defer wg.Done() - var companyName = "" - var userName = "" - var userLFID = "" - var userGHID = "" - var userGHUsername = "" - var swg sync.WaitGroup - swg.Add(2) - - go func() { - defer swg.Done() - if sigModel.SignatureReferenceType == "user" { - userModel, userErr := repo.usersRepo.GetUser(sigModel.SignatureReferenceID.String()) - if userErr != nil || userModel == nil { - log.WithFields(f).Warnf("unable to lookup user using id: %s, error: %v", sigModel.SignatureReferenceID, userErr) - } else { - userName = userModel.Username - userLFID = userModel.LfUsername - userGHID = userModel.GithubID - userGHUsername = userModel.GithubUsername - } + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if approved == nil && signed == nil && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } - if signatureUserCompanyID != "" { - dbCompanyModel, companyErr := repo.companyRepo.GetCompany(ctx, signatureUserCompanyID) - if companyErr != nil { - log.WithFields(f).Warnf("unable to lookup company using id: %s, error: %v", signatureUserCompanyID, companyErr) - } else { - companyName = dbCompanyModel.CompanyName - } - } - } else if sigModel.SignatureReferenceType == "company" { - dbCompanyModel, companyErr := repo.companyRepo.GetCompany(ctx, sigModel.SignatureReferenceID.String()) - if companyErr != nil { - log.WithFields(f).Warnf("unable to lookup company using id: %s, error: %v", sigModel.SignatureReferenceID, companyErr) - } else { - companyName = dbCompanyModel.CompanyName - } - } - }() - - var signatureACL []models.User - go func() { - defer swg.Done() - for _, userName := range sigACL { - if loadACLDetails { - userModel, userErr := repo.usersRepo.GetUserByUserName(userName, true) - if userErr != nil { - log.WithFields(f).Warnf("unable to lookup user using username: %s, error: %v", userName, userErr) - } else { - if userModel == nil { - log.WithFields(f).Warnf("User looking for username is null: %s for signature: %s", userName, sigModel.SignatureID) - } else { - signatureACL = append(signatureACL, *userModel) - } - } - } else { - signatureACL = append(signatureACL, models.User{LfUsername: userName}) - } - } - }() - swg.Wait() - sigModel.CompanyName = companyName - sigModel.UserName = userName - sigModel.UserLFID = userLFID - sigModel.UserGHID = userGHID - sigModel.UserGHUsername = userGHUsername - sigModel.SignatureACL = signatureACL - }(sig, dbSignature.SignatureUserCompanyID, dbSignature.SignatureACL) - } - wg.Wait() - return sigs, nil -} + if searchTerm != nil { + searchTermValue := utils.StringValue(searchTerm) + f["searchTerm"] = searchTermValue + log.WithFields(f).Debugf("adding search term filter for: '%s'", searchTermValue) + searchTermExpression := expression.Name("signature_reference_name_lower").Contains(strings.ToLower(searchTermValue)). + Or(expression.Name("user_email").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("user_lf_username").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("user_name").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name(SignatureUserGitHubUsername).Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("user_docusign_name").Contains(strings.ToLower(searchTermValue))) + filter = addAndCondition(filter, searchTermExpression, &filterAdded) + } -// buildResponse is a helper function which converts a database model to a GitHub organization response model -func buildResponse(items []*dynamodb.AttributeValue) []models.GithubOrg { - // Convert to a response model - var orgs []models.GithubOrg - for _, org := range items { - selected := true - orgs = append(orgs, models.GithubOrg{ - ID: org.S, - Selected: &selected, - }) + //log.WithFields(f).Debugf("filter: %+v", filter) + + // Use the builder to create the expression + expr, err := expression.NewBuilder(). + WithKeyCondition(condition). + WithFilter(filter). + WithProjection(buildProjection()). + Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for get cla group icla signatures, claGroupID: %s, error: %v", + claGroupID, err) + return nil, err } - return orgs -} + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String(SignatureProjectIDIndex), + } -// buildApprovalAttributeList builds the updated approval list based on the added and removed values -func buildApprovalAttributeList(ctx context.Context, existingList, addEntries, removeEntries []string) *dynamodb.AttributeValue { - f := logrus.Fields{ - "functionName": "buildApprovalAttributeList", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + if pageSize == 0 { + pageSize = DefaultPageSize } - var updatedList []string - log.WithFields(f).Debugf("buildApprovalAttributeList - existing: %+v, add entries: %+v, remove entries: %+v", - existingList, addEntries, removeEntries) - // Add the existing entries to our response - for _, value := range existingList { - // No duplicates allowed - if !utils.StringInSlice(value, updatedList) { - log.WithFields(f).Debugf("buildApprovalAttributeList - adding existing entry: %s", value) - updatedList = append(updatedList, strings.TrimSpace(value)) - } else { - log.WithFields(f).Debugf("buildApprovalAttributeList - skipping existing entry: %s", value) - } + if pageSize > BigPageSize { + pageSize = BigPageSize } - // For all the new values... - for _, value := range addEntries { - // No duplicates allowed - if !utils.StringInSlice(value, updatedList) { - log.WithFields(f).Debugf("buildApprovalAttributeList - adding new entry: %s", value) - updatedList = append(updatedList, strings.TrimSpace(value)) - } else { - log.WithFields(f).Debugf("buildApprovalAttributeList - skipping new entry: %s", value) + queryInput.Limit = &pageSize + + // If we have the next key, set the exclusive start key value + if nextKey != "" { + // log.WithFields(f).Debugf("Received a nextKey, value: %s", nextKey) + // The primary key of the first item that this operation will evaluate. + // and the query key (if not the same) + queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(nextKey), + }, + "signature_project_id": { + S: aws.String(claGroupID), + }, } } - // Remove the items - log.WithFields(f).Debugf("buildApprovalAttributeList - before: %+v - removing entries: %+v", updatedList, removeEntries) - updatedList = utils.RemoveItemsFromList(updatedList, removeEntries) - log.WithFields(f).Debugf("buildApprovalAttributeList - after: %+v - removing entries: %+v", updatedList, removeEntries) + var intermediateResponse []*iclaSignatureWithDetails + var lastEvaluatedKey string + // Loop until we have all the records + for ok := true; ok; ok = lastEvaluatedKey != "" { + // Make the DynamoDB Query API call + results, errQuery := repo.dynamoDBClient.Query(queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error retrieving icla signatures for project: %s , error: %v", + claGroupID, errQuery) + return nil, errQuery + } - // Remove any duplicates - shouldn't have any if checked before adding - log.WithFields(f).Debugf("buildApprovalAttributeList - before: %+v - removing duplicates", updatedList) - updatedList = utils.RemoveDuplicates(updatedList) - log.WithFields(f).Debugf("buildApprovalAttributeList - after: %+v - removing duplicates", updatedList) + var dbSignatures []ItemSignature - // Convert to the response type - var responseList []*dynamodb.AttributeValue - for _, value := range updatedList { - responseList = append(responseList, &dynamodb.AttributeValue{S: aws.String(value)}) - } + unmarshallError := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) + if unmarshallError != nil { + log.WithFields(f).Warnf("error unmarshalling icla signatures from database for cla group: %s, error: %v", + claGroupID, unmarshallError) + return nil, unmarshallError + } - return &dynamodb.AttributeValue{L: responseList} -} + intermediateResponse = append(intermediateResponse, repo.getIntermediateICLAResponse(f, dbSignatures)...) -// buildCompanyIDList is a helper function to convert the DB response models into a simple list of company IDs -func (repo repository) buildCompanyIDList(ctx context.Context, results *dynamodb.QueryOutput) ([]SignatureCompanyID, error) { - f := logrus.Fields{ - "functionName": "buildCompanyIDList", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + //log.WithFields(f).Debugf("LastEvaluatedKey: %+v", results.LastEvaluatedKey["signature_id"]) + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" + } + + if int64(len(intermediateResponse)) >= pageSize { + break + } } - var response []SignatureCompanyID - // The DB signature model - var dbSignatures []ItemSignature - err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) - if err != nil { - log.WithFields(f).Warnf("error unmarshalling signatures from database, error: %v", err) - return nil, err + if int64(len(intermediateResponse)) > pageSize { + intermediateResponse = intermediateResponse[0:pageSize] + lastEvaluatedKey = intermediateResponse[pageSize-1].IclaSignature.SignatureID } - // Loop and extract the company ID (signature_reference_id) value - for _, item := range dbSignatures { - // Lookup the company by ID - try to get more information like the external ID and name - companyModel, companyLookupErr := repo.companyRepo.GetCompany(ctx, item.SignatureReferenceID) - // Start building a model for this entry in the list - signatureCompanyID := SignatureCompanyID{ - SignatureID: item.SignatureID, - CompanyID: item.SignatureReferenceID, - } + // Append all the responses to our list + out := &models.IclaSignatures{ + LastKeyScanned: lastEvaluatedKey, + PageSize: pageSize, + ResultCount: int64(len(intermediateResponse)), + } - if companyLookupErr != nil || companyModel == nil { - log.WithFields(f).Warnf("problem looking up company using id: %s, error: %+v", - item.SignatureReferenceID, companyLookupErr) - response = append(response, signatureCompanyID) - } else { - if companyModel.CompanyExternalID != "" { - signatureCompanyID.CompanySFID = companyModel.CompanyExternalID - } - if companyModel.CompanyName != "" { - signatureCompanyID.CompanyName = companyModel.CompanyName - } - response = append(response, signatureCompanyID) + var iclaSignatures []*models.IclaSignature + if withExtraDetails { + iclaSignatures, err = repo.addAdditionalICLAMetaData(f, intermediateResponse) + } else { + for _, sig := range intermediateResponse { + iclaSignatures = append(iclaSignatures, sig.IclaSignature) } } + if err != nil { + return nil, err + } - return response, nil + out.List = iclaSignatures + return out, nil } -func (repo repository) GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string) (*models.IclaSignatures, error) { +func (repo repository) GetICLAByDate(ctx context.Context, startDate string) ([]ItemSignature, error) { f := logrus.Fields{ - "functionName": "GetClaGroupICLASignatures", + "functionName": "v1.signatures.repository.GetICLAs", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, + "startDate": startDate, } - sortKeyPrefix := fmt.Sprintf("%s#%v#%v", utils.ClaTypeICLA, true, true) - // This is the key we want to match - condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)). - And(expression.Key("sigtype_signed_approved_id").BeginsWith(sortKeyPrefix)) + var signatures []ItemSignature - // Use the builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + log.WithFields(f).Debug("querying for icla signatures by date...") + + filter := expression.Name("date_created").GreaterThanEqual(expression.Value(startDate)). + And(expression.Name("signature_type").Equal(expression.Value(utils.SignatureTypeCLA))). + And(expression.Name("signature_signed").Equal(expression.Value(true))). + And(expression.Name("signature_approved").Equal(expression.Value(true))) + + // Use the expression builder to create the expression + expr, err := expression.NewBuilder().WithFilter(filter).Build() if err != nil { - log.WithFields(f).Warnf("error building expression for get cla group icla signatures, claGroupID: %s, error: %v", - claGroupID, err) + log.WithFields(f).Warnf("error building expression for query icla signatures by date: %v", err) return nil, err } - // Assemble the query input parameters - queryInput := &dynamodb.QueryInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - KeyConditionExpression: expr.KeyCondition(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(repo.signatureTableName), - IndexName: aws.String(SignatureProjectIDSigTypeSignedApprovedIDIndex), - Limit: aws.Int64(HugePageSize), - } - out := &models.IclaSignatures{List: make([]*models.IclaSignature, 0)} - if searchTerm != nil { - searchTerm = aws.String(strings.ToLower(*searchTerm)) - } + var lastEvaluatedKey map[string]*dynamodb.AttributeValue + for { - // Make the DynamoDB Query API call - results, queryErr := repo.dynamoDBClient.Query(queryInput) - if queryErr != nil { - log.WithFields(f).Warnf("error retrieving icla signatures for project: %s, error: %v", claGroupID, queryErr) - return nil, queryErr + scanInput := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.signatureTableName), + ExclusiveStartKey: lastEvaluatedKey, } - var dbSignatures []ItemSignature - - err := dynamodbattribute.UnmarshalListOfMaps(results.Items, &dbSignatures) + result, err := repo.dynamoDBClient.Scan(scanInput) if err != nil { - log.WithFields(f).Warnf("error unmarshalling icla signatures from database for cla group: %s, error: %v", - claGroupID, err) + log.WithFields(f).Warnf("error retrieving icla signatures by date: %v", err) return nil, err } - for _, sig := range dbSignatures { - if searchTerm != nil { - if !strings.Contains(sig.SignatureReferenceNameLower, *searchTerm) { - continue - } + log.WithFields(f).Debugf("retrieved %d icla signatures by date", len(result.Items)) + + var dbSignatures []ItemSignature + + unmarshallError := dynamodbattribute.UnmarshalListOfMaps(result.Items, &dbSignatures) + if unmarshallError != nil { + log.WithFields(f).Warnf("error unmarshalling icla signatures from database by date: %v", unmarshallError) + return nil, unmarshallError + } + + signatures = append(signatures, dbSignatures...) + + // log.WithFields(f).Debugf("last evaluated key: %+v", result.LastEvaluatedKey) + + if result.LastEvaluatedKey == nil { + break + } + lastEvaluatedKey = result.LastEvaluatedKey + } + + return signatures, nil +} + +func (repo repository) getIntermediateICLAResponse(f logrus.Fields, dbSignatures []ItemSignature) []*iclaSignatureWithDetails { + var intermediateResponse []*iclaSignatureWithDetails + + for _, sig := range dbSignatures { + // Set the signed date/time + var sigSignedTime string + // Use the user docusign date signed value if it is present - older signatures do not have this + if sig.UserDocusignDateSigned != "" { + // Put the date into a standard format + t, err := utils.ParseDateTime(sig.UserDocusignDateSigned) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to parse signature docusign date signed time: %s", sig.UserDocusignDateSigned) + } else { + sigSignedTime = utils.TimeToString(t) } - signedOn := sig.DateCreated - if sig.SignedOn != "" { - signedOn = sig.SignedOn + } else if sig.DateCreated != "" { + // Put the date into a standard format + t, err := utils.ParseDateTime(sig.DateCreated) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to parse signature date created time: %s", sig.DateCreated) + } else { + sigSignedTime = utils.TimeToString(t) } - out.List = append(out.List, &models.IclaSignature{ + } + + intermediateResponse = append(intermediateResponse, &iclaSignatureWithDetails{ + IclaSignature: &models.IclaSignature{ GithubUsername: sig.UserGithubUsername, + GitlabUsername: sig.UserGitlabUsername, + UserID: sig.SignatureReferenceID, LfUsername: sig.UserLFUsername, + SignatureApproved: sig.SignatureApproved, + SignatureSigned: sig.SignatureSigned, + SignatureModified: sig.DateModified, SignatureID: sig.SignatureID, + SignedOn: sigSignedTime, + UserDocusignDateSigned: sigSignedTime, + UserDocusignName: sig.UserDocusignName, UserEmail: sig.UserEmail, UserName: sig.UserName, - SignedOn: signedOn, - UserDocusignName: sig.UserDocusignName, - UserDocusignDateSigned: sig.UserDocusignDateSigned, - SignatureModified: sig.DateModified, - }) - } + }, + SignatureReferenceID: sig.SignatureReferenceID, + }) + } - if len(results.LastEvaluatedKey) == 0 { - break + return intermediateResponse +} + +func (repo repository) addAdditionalICLAMetaData(f logrus.Fields, intermediateResponse []*iclaSignatureWithDetails) ([]*models.IclaSignature, error) { + // For some older ICLA signatures, we are missing the user's info, but we have their internal ID - let's look up those values before returning + responseChannel := make(chan *models.IclaSignature) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + for _, iclaDetails := range intermediateResponse { + go func(iclaSignatureWithDetails *iclaSignatureWithDetails) { + userModel, userLookupErr := repo.usersRepo.GetUser(iclaSignatureWithDetails.SignatureReferenceID) + if userLookupErr != nil || userModel == nil { + log.WithFields(f).WithError(userLookupErr).Warnf("unable to lookup user with id: %s", iclaSignatureWithDetails.SignatureReferenceID) + } else { + // If the GitHub username is empty, see if it was set in the user model + if iclaSignatureWithDetails.IclaSignature.GithubUsername == "" { + // Grab and set the GitHub username from the user model + iclaSignatureWithDetails.IclaSignature.GithubUsername = userModel.GithubUsername + } + // If the GitLab username is empty, see if it was set in the user model + if iclaSignatureWithDetails.IclaSignature.GitlabUsername == "" { + // Grab and set the GitLab username from the user model + iclaSignatureWithDetails.IclaSignature.GitlabUsername = userModel.GitlabUsername + } + // If the username is empty, see if it was set in the user model + if iclaSignatureWithDetails.IclaSignature.UserName == "" { + if userModel.Username != "" { + // Grab and set the username + iclaSignatureWithDetails.IclaSignature.UserName = userModel.Username + } else if userModel.LfUsername != "" { + iclaSignatureWithDetails.IclaSignature.UserName = userModel.LfUsername + } + } + // If the user email is empty, see if it was set in the user model + if iclaSignatureWithDetails.IclaSignature.UserEmail == "" { + // Grab and set the email from the user record + iclaSignatureWithDetails.IclaSignature.UserEmail = getBestEmail(userModel) + } + } + + responseChannel <- iclaSignatureWithDetails.IclaSignature + }(iclaDetails) + } + + var finalResults []*models.IclaSignature + for i := 0; i < len(intermediateResponse); i++ { + select { + case result := <-responseChannel: + finalResults = append(finalResults, result) + case <-ctx.Done(): + log.WithError(ctx.Err()).Warnf("timeout during adding additional meta to icla signatures") + return nil, ctx.Err() } - queryInput.ExclusiveStartKey = results.LastEvaluatedKey - log.WithFields(f).Debug("querying next page") } - return out, nil + + return finalResults, nil } -func (repo repository) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, searchTerm *string) (*models.CorporateContributorList, error) { +func (repo repository) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey *string, searchTerm *string) (*models.CorporateContributorList, error) { f := logrus.Fields{ - "functionName": "GetClaGroupCorporateContributors", + "functionName": "v1.signatures.repository.GetClaGroupCorporateContributors", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "companyID": aws.StringValue(companyID), } + totalCountChannel := make(chan int64, 1) + go repo.getTotalCorporateContributorCount(ctx, claGroupID, companyID, searchTerm, totalCountChannel) + + totalCount := <-totalCountChannel + log.WithFields(f).Debugf("total corporate contributor count: %d", totalCount) + // If the page size is nil, set it to the default + if pageSize == nil { + pageSize = aws.Int64(10) + } + + if *pageSize > totalCount { + pageSize = aws.Int64(totalCount) + } + + log.WithFields(f).Debugf("total corporate contributor count: %d, page size: %d", totalCount, *pageSize) + condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)) - if companyID != nil { - sortKey := fmt.Sprintf("%s#%v#%v#%v", utils.ClaTypeECLA, true, true, *companyID) - condition = condition.And(expression.Key("sigtype_signed_approved_id").Equal(expression.Value(sortKey))) - } else { - sortKeyPrefix := fmt.Sprintf("%s#%v#%v", utils.ClaTypeECLA, true, true) - condition = condition.And(expression.Key("sigtype_signed_approved_id").BeginsWith(sortKeyPrefix)) + // if companyID != nil { + // sortKey := fmt.Sprintf("%s#%v#%v#%v", utils.ClaTypeECLA, true, true, *companyID) + // condition = condition.And(expression.Key("sigtype_signed_approved_id").Equal(expression.Value(sortKey))) + // } else { + // sortKeyPrefix := fmt.Sprintf("%s#%v#%v", utils.ClaTypeECLA, true, true) + // condition = condition.And(expression.Key("sigtype_signed_approved_id").BeginsWith(sortKeyPrefix)) + // } + filter := expression.Name("signature_user_ccla_company_id").Equal(expression.Value(companyID)) + // filter = filter.And(expression.Name("signature_type").Equal(expression.Value(utils.ClaTypeECLA))) + + // Create our builder + builder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).WithFilter(filter) + + if searchTerm != nil { + searchTermValue := utils.StringValue(searchTerm) + f["searchTerm"] = searchTermValue + log.WithFields(f).Debugf("adding search term filter for: '%s'", searchTermValue) + builder.WithFilter(expression.Name("signature_reference_name_lower").Contains(strings.ToLower(searchTermValue)). + Or(expression.Name("user_email").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("github_username").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("userDocusignName").Contains(strings.ToLower(searchTermValue)))) } // Use the builder to create the expression - expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build() + expr, err := builder.Build() if err != nil { log.WithFields(f).Warnf("error building expression for get cla group icla signatures, claGroupID: %s, error: %v", claGroupID, err) @@ -2441,22 +4678,35 @@ func (repo repository) GetClaGroupCorporateContributors(ctx context.Context, cla ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), TableName: aws.String(repo.signatureTableName), - IndexName: aws.String(SignatureProjectIDSigTypeSignedApprovedIDIndex), - Limit: aws.Int64(HugePageSize), + IndexName: aws.String(SignatureProjectIDIndex), + Limit: aws.Int64(*pageSize), } - out := &models.CorporateContributorList{List: make([]*models.CorporateContributor, 0)} - if searchTerm != nil { - searchTerm = aws.String(strings.ToLower(*searchTerm)) + if nextKey != nil { + log.WithFields(f).Debugf("adding next key to query input: %s", *nextKey) + queryInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "signature_project_id": { + S: aws.String(claGroupID), + }, + "signature_id": { + S: aws.String(*nextKey), + }, + } } - for { + out := &models.CorporateContributorList{List: make([]*models.CorporateContributor, 0)} + var lastEvaluatedKey string + + currentCount := int64(0) + + for ok := true; ok; ok = lastEvaluatedKey != "" && currentCount < *pageSize { // Make the DynamoDB Query API call log.WithFields(f).Debug("querying signatures...") results, queryErr := repo.dynamoDBClient.Query(queryInput) if queryErr != nil { - log.WithFields(f).Warnf("error retrieving icla signatures for project: %s, error: %v", claGroupID, queryErr) + log.WithFields(f).Warnf("error retrieving ecla signatures for project: %s, error: %v", claGroupID, queryErr) return nil, queryErr } @@ -2469,42 +4719,331 @@ func (repo repository) GetClaGroupCorporateContributors(ctx context.Context, cla return nil, err } + log.WithFields(f).Debugf("located %d signatures...", len(dbSignatures)) for _, sig := range dbSignatures { - if searchTerm != nil { - if !strings.Contains(sig.SignatureReferenceNameLower, *searchTerm) { - continue - } - } var sigCreatedTime = sig.DateCreated t, err := utils.ParseDateTime(sig.DateCreated) if err != nil { - log.Error("fillCorporateContributorModel: unable to parse time", err) + log.WithFields(f).WithError(err).Warn("unable to parse signature date created time") } else { sigCreatedTime = utils.TimeToString(t) } - signatureVersion := fmt.Sprintf("v%s.%s", sig.SignatureDocumentMajorVersion, sig.SignatureDocumentMinorVersion) + + // Set the signed date/time + var sigSignedTime string + // Use the user docusign date signed value if it is present - older signatures do not have this + if sig.UserDocusignDateSigned != "" { + // Put the date into a standard format + t, err = utils.ParseDateTime(sig.UserDocusignDateSigned) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to parse signature docusign date signed time") + } else { + sigSignedTime = utils.TimeToString(t) + } + } else { + // Put the date into a standard format + t, err = utils.ParseDateTime(sig.DateCreated) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to parse signature date created time") + } else { + sigSignedTime = utils.TimeToString(t) + } + } + + signatureVersion := fmt.Sprintf("v%s.%s", strconv.Itoa(sig.SignatureDocumentMajorVersion), strconv.Itoa(sig.SignatureDocumentMinorVersion)) + + sigName := sig.UserName + user, userErr := repo.usersRepo.GetUser(sig.SignatureReferenceID) + if userErr != nil { + log.WithFields(f).Warnf("unable to get user for id: %s, error: %v ", sig.SignatureReferenceID, userErr) + } + if user != nil && sigName == "" { + sigName = user.Username + } + out.List = append(out.List, &models.CorporateContributor{ + SignatureID: sig.SignatureID, GithubID: sig.UserGithubUsername, LinuxFoundationID: sig.UserLFUsername, - Name: sig.UserName, + Name: sigName, SignatureVersion: signatureVersion, Email: sig.UserEmail, Timestamp: sigCreatedTime, UserDocusignName: sig.UserDocusignName, - UserDocusignDateSigned: sig.UserDocusignDateSigned, + UserDocusignDateSigned: sigSignedTime, SignatureModified: sig.DateModified, + SignatureApproved: sig.SignatureApproved, + SignatureSigned: sig.SignatureSigned, }) + + // Increment the current count + currentCount++ + if currentCount >= *pageSize { + break + } } - if len(results.LastEvaluatedKey) == 0 { - break + if results.LastEvaluatedKey["signature_id"] != nil && currentCount < *pageSize { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" } - queryInput.ExclusiveStartKey = results.LastEvaluatedKey - log.WithFields(f).Debug("querying next page") + } sort.Slice(out.List, func(i, j int) bool { return out.List[i].Name < out.List[j].Name }) + out.ResultCount = currentCount + out.TotalCount = totalCount + out.NextKey = lastEvaluatedKey + return out, nil } + +func (repo repository) getTotalCorporateContributorCount(ctx context.Context, claGroupID string, companyID, searchTerm *string, totalCountChannel chan int64) { + f := logrus.Fields{ + "functionName": "v1.signature.repository.getTotalCorporateContributorCount", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "companyID": companyID, + "searchTerm": searchTerm, + } + + pageSize := int64(HugePageSize) + f["pageSize"] = pageSize + + condition := expression.Key("signature_project_id").Equal(expression.Value(claGroupID)) + + filter := expression.Name("signature_user_ccla_company_id").Equal(expression.Value(companyID)).And(expression.Name("signature_approved").Equal(expression.Value(true))).And(expression.Name("signature_signed").Equal(expression.Value(true))) + + builder := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter) + + if searchTerm != nil { + searchTermValue := *searchTerm + builder = builder.WithFilter(expression.Name("user_name").Contains(strings.ToLower(searchTermValue)). + Or(expression.Name("user_email").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("github_username").Contains(strings.ToLower(searchTermValue))). + Or(expression.Name("userDocusignName").Contains(strings.ToLower(searchTermValue)))) + } + + beforeQuery, _ := utils.CurrentTime() + log.WithFields(f).Debugf("running total signature count query for claGroupID: %s, companyID: %s", claGroupID, *companyID) + + expr, err := builder.WithProjection(buildCountProjection()).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for cla group: %s, error: %v", claGroupID, err) + totalCountChannel <- 0 + return + } + + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String(SignatureProjectIDIndex), + Limit: aws.Int64(pageSize), + } + + var lastEvaluatedKey string + var totalCount int64 + + // Loop until we have all the records - we'll get a nil lastEvaluatedKey when we're done + for ok := true; ok; ok = lastEvaluatedKey != "" { + results, errQuery := repo.dynamoDBClient.QueryWithContext(ctx, queryInput) + if errQuery != nil { + log.WithFields(f).Warnf("error querying signatures for cla group: %s, error: %v", claGroupID, errQuery) + totalCountChannel <- 0 + return + } + + // Add the count to the total + totalCount += *results.Count + + // Set the last evaluated key + if results.LastEvaluatedKey["signature_id"] != nil { + lastEvaluatedKey = *results.LastEvaluatedKey["signature_id"].S + queryInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + lastEvaluatedKey = "" + } + } + + log.WithFields(f).Debugf("total signature count query took: %s", time.Since(beforeQuery)) + + totalCountChannel <- totalCount + +} + +// EclaAutoCreate this routine updates the CCLA signature record by adjusting the auto_create_ecla column to the specified value +func (repo repository) EclaAutoCreate(ctx context.Context, signatureID string, autoCreateECLA bool) error { + f := logrus.Fields{ + "functionName": "v1.signature.repository.EclaAutoCreate", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + "autoCreateECLA": autoCreateECLA, + } + + // Build the expression + expressionUpdate := expression.Set(expression.Name("auto_create_ecla"), expression.Value(autoCreateECLA)) + + expr, err := expression.NewBuilder().WithUpdate(expressionUpdate).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for signature: %s, error: %v", signatureID, err) + return err + } + + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + Key: map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(signatureID), + }, + }, + ConditionExpression: expr.KeyCondition(), + TableName: aws.String(repo.signatureTableName), + UpdateExpression: expr.Update(), + } + + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).Warnf("error updating signature: %s, error: %v", signatureID, updateErr) + return updateErr + } + + return nil +} + +// ActivateSignature used to activate signature again, in case of deactivated signature found +func (repo repository) ActivateSignature(ctx context.Context, signatureID string) error { + f := logrus.Fields{ + "functionName": "v1.signature.repository.ActivateSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signatureID": signatureID, + } + + // Build the expression + expressionUpdate := expression.Set(expression.Name("signature_approved"), expression.Value(true)).Set(expression.Name("signature_signed"), expression.Value(false)) + + expr, err := expression.NewBuilder().WithUpdate(expressionUpdate).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for signature: %s, error: %v", signatureID, err) + return err + } + + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + Key: map[string]*dynamodb.AttributeValue{ + "signature_id": { + S: aws.String(signatureID), + }, + }, + ConditionExpression: expr.KeyCondition(), + TableName: aws.String(repo.signatureTableName), + UpdateExpression: expr.Update(), + } + + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).Warnf("error updating signature: %s, error: %v", signatureID, updateErr) + return updateErr + } + return nil +} + +// getGerritUsers is a helper function to fetch the list of gerrit users for the specified type - results are returned through the specified results channel +// func (repo repository) getGerritUsers(ctx context.Context, authUser *auth.User, projectSFID string, claType string, gerritResultChannel chan *GerritUserResponse) { +// // f := logrus.Fields{ +// // "functionName": "v1.signatures.repository.getGerritUsers", +// // utils.XREQUESTID: ctx.Value(utils.XREQUESTID), +// // "projectSFID": projectSFID, +// // } +// // log.WithFields(f).Debugf("querying gerrit for %s gerrit users...", claType) +// // gerritIclaUsers, getGerritQueryErr := repo.gerritService.GetUsersOfGroup(ctx, authUser, projectSFID, claType) +// // if getGerritQueryErr != nil || gerritIclaUsers == nil { +// // msg := fmt.Sprintf("unable to fetch gerrit users for claGroup: %s , claType: %s ", projectSFID, claType) +// // log.WithFields(f).WithError(getGerritQueryErr).Warn(msg) +// // gerritResultChannel <- &GerritUserResponse{ +// // gerritGroupResponse: nil, +// // queryType: claType, +// // Error: errors.New(msg), +// // } +// // return +// // } + +// // log.WithFields(f).Debugf("retrieved %d gerrit users for CLA type: %s...", len(gerritIclaUsers.Members), claType) +// gerritResultChannel <- &GerritUserResponse{ +// gerritGroupResponse: nil, +// queryType: claType, +// Error: nil, +// } +// } + +func buildNextKey(indexName string, signature *models.Signature) (string, error) { + nextKey := make(map[string]*dynamodb.AttributeValue) + nextKey["signature_id"] = &dynamodb.AttributeValue{S: aws.String(signature.SignatureID)} + switch indexName { + // TODO: review all these use-cases + case SignatureProjectIDIndex: + nextKey["signature_project_id"] = &dynamodb.AttributeValue{S: aws.String(signature.ProjectID)} + case SignatureProjectDateIDIndex: + nextKey["signature_project_id"] = &dynamodb.AttributeValue{S: aws.String(signature.ProjectID)} + nextKey["date_modified"] = &dynamodb.AttributeValue{S: aws.String(signature.SignatureModified)} + case SignatureProjectReferenceIndex: + nextKey["signature_project_id"] = &dynamodb.AttributeValue{S: aws.String(signature.ProjectID)} + nextKey["signature_reference_id"] = &dynamodb.AttributeValue{S: aws.String(signature.SignatureReferenceID)} + case SignatureProjectIDSigTypeSignedApprovedIDIndex: + nextKey["signature_project_id"] = &dynamodb.AttributeValue{S: aws.String(signature.ProjectID)} + nextKey["signature_signed_approved_id"] = &dynamodb.AttributeValue{S: aws.String(fmt.Sprintf("%t#%t", signature.SignatureSigned, signature.SignatureApproved))} + case SignatureProjectIDTypeIndex: + nextKey["signature_project_id"] = &dynamodb.AttributeValue{S: aws.String(signature.ProjectID)} + nextKey["signature_type"] = &dynamodb.AttributeValue{S: aws.String(signature.SignatureType)} + case SignatureReferenceIndex: + nextKey["signature_reference_id"] = &dynamodb.AttributeValue{S: aws.String(signature.SignatureReferenceID)} + case SignatureReferenceSearchIndex: + nextKey["signature_project_id"] = &dynamodb.AttributeValue{S: aws.String(signature.ProjectID)} + nextKey["signature_reference_name_lower"] = &dynamodb.AttributeValue{S: aws.String(signature.SignatureReferenceNameLower)} + } + + return encodeNextKey(nextKey) +} + +// encodeNextKey encodes the map as a string +func encodeNextKey(in map[string]*dynamodb.AttributeValue) (string, error) { + if len(in) == 0 { + return "", nil + } + b, err := json.Marshal(in) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(b), nil +} + +// decodeNextKey decodes the next key value into a dynamodb attribute value +func decodeNextKey(str string) (map[string]*dynamodb.AttributeValue, error) { + f := logrus.Fields{ + "functionName": "v1.events.repository.decodeNextKey", + } + + sDec, err := base64.StdEncoding.DecodeString(str) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error decoding string %s", str) + return nil, err + } + + var m map[string]*dynamodb.AttributeValue + err = json.Unmarshal(sDec, &m) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error unmarshalling string after decoding: %s", sDec) + return nil, err + } + + return m, nil +} diff --git a/cla-backend-go/signatures/service.go b/cla-backend-go/signatures/service.go index fa13c9160..f4295df4c 100644 --- a/cla-backend-go/signatures/service.go +++ b/cla-backend-go/signatures/service.go @@ -7,54 +7,77 @@ import ( "context" "errors" "fmt" + "regexp" + "strconv" + "strings" "sync" "github.com/aws/aws-sdk-go/aws" + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + + "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + "github.com/communitybridge/easycla/cla-backend-go/repositories" "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/events" - "github.com/communitybridge/easycla/cla-backend-go/users" "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/company" "github.com/communitybridge/easycla/cla-backend-go/utils" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + "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/gen/models" - githubpkg "github.com/google/go-github/v33/github" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + githubpkg "github.com/google/go-github/v37/github" "golang.org/x/oauth2" ) // SignatureService interface type SignatureService interface { GetSignature(ctx context.Context, signatureID string) (*models.Signature, error) - GetIndividualSignature(ctx context.Context, claGroupID, userID string) (*models.Signature, error) - GetCorporateSignature(ctx context.Context, claGroupID, companyID string) (*models.Signature, error) + GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) + GetIndividualSignatures(ctx context.Context, claGroupID, userID string, approved, signed *bool) ([]*models.Signature, error) + GetCorporateSignature(ctx context.Context, claGroupID, companyID string, approved, signed *bool) (*models.Signature, error) + GetCorporateSignatures(ctx context.Context, claGroupID, companyID string, approved, signed *bool) ([]*models.Signature, error) GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams) (*models.Signatures, error) - GetProjectCompanySignature(ctx context.Context, companyID, projectID string, signed, approved *bool, nextKey *string, pageSize *int64) (*models.Signature, error) + GetCCLASignatures(ctx context.Context, signed, approved *bool) ([]*ItemSignature, error) + CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) + GetProjectCompanySignature(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, pageSize *int64) (*models.Signature, error) GetProjectCompanySignatures(ctx context.Context, params signatures.GetProjectCompanySignaturesParams) (*models.Signatures, error) - GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams) (*models.Signatures, error) + GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *ApprovalCriteria) (*models.Signatures, error) GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams) (*models.Signatures, error) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]SignatureCompanyID, error) - GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams) (*models.Signatures, error) - InvalidateProjectRecords(ctx context.Context, projectID string, projectName string) (int, error) - - GetGithubOrganizationsFromWhitelist(ctx context.Context, signatureID string, githubAccessToken string) ([]models.GithubOrg, error) - AddGithubOrganizationToWhitelist(ctx context.Context, signatureID string, whiteListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) - DeleteGithubOrganizationFromWhitelist(ctx context.Context, signatureID string, whiteListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) - UpdateApprovalList(ctx context.Context, authUser *auth.User, claGroupModel *models.ClaGroup, companyModel *models.Company, claGroupID string, params *models.ApprovalList) (*models.Signature, error) + GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, projectID *string) (*models.Signatures, error) + InvalidateProjectRecords(ctx context.Context, projectID, note string) (int, error) + CreateSignature(ctx context.Context, signature *ItemSignature) error + UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error + SaveOrUpdateSignature(ctx context.Context, signature *ItemSignature) error + HasUserSigned(ctx context.Context, user *models.User, projectID string) (*bool, *bool, error) + + GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string, githubAccessToken string) ([]models.GithubOrg, error) + AddGithubOrganizationToApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) + DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) + UpdateApprovalList(ctx context.Context, authUser *auth.User, claGroupModel *models.ClaGroup, companyModel *models.Company, claGroupID string, params *models.ApprovalList, projectSFID string) (*models.Signature, error) AddCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error) RemoveCLAManager(ctx context.Context, ignatureID, claManagerID string) (*models.Signature, error) - GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string) (*models.IclaSignatures, error) - GetClaGroupCCLASignatures(ctx context.Context, claGroupID string) (*models.Signatures, error) - GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, searchTerm *string) (*models.CorporateContributorList, error) + GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) + GetClaGroupCCLASignatures(ctx context.Context, claGroupID string, approved, signed *bool) (*models.Signatures, error) + GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey *string, searchTerm *string) (*models.CorporateContributorList, error) + + // createOrGetEmployeeModels(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) + CreateOrUpdateEmployeeSignature(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) + UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error) + // handleGitHubStatusUpdate(ctx context.Context, employeeUserModel *models.User) error + ProcessEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, user *models.User) (*bool, error) + UserIsApproved(ctx context.Context, user *models.User, cclaSignature *models.Signature) (bool, error) } type service struct { @@ -63,16 +86,30 @@ type service struct { usersService users.Service eventsService events.Service githubOrgValidation bool + repositoryService repositories.Service + githubOrgService github_organizations.ServiceInterface + claGroupService service2.Service + gitLabApp *gitlab_api.App + claBaseAPIURL string + claLandingPage string + claLogoURL string } -// NewService creates a new whitelist service -func NewService(repo SignatureRepository, companyService company.IService, usersService users.Service, eventsService events.Service, githubOrgValidation bool) SignatureService { +// NewService creates a new signature service +func NewService(repo SignatureRepository, companyService company.IService, usersService users.Service, eventsService events.Service, githubOrgValidation bool, repositoryService repositories.Service, githubOrgService github_organizations.ServiceInterface, claGroupService service2.Service, gitLabApp *gitlab_api.App, CLABaseAPIURL, CLALandingPage, CLALogoURL string) SignatureService { return service{ repo, companyService, usersService, eventsService, githubOrgValidation, + repositoryService, + githubOrgService, + claGroupService, + gitLabApp, + CLABaseAPIURL, + CLALandingPage, + CLALogoURL, } } @@ -81,26 +118,56 @@ func (s service) GetSignature(ctx context.Context, signatureID string) (*models. return s.repo.GetSignature(ctx, signatureID) } +// SaveOrUpdateSignature saves or updates the specified signature +func (s service) SaveOrUpdateSignature(ctx context.Context, signature *ItemSignature) error { + return s.repo.SaveOrUpdateSignature(ctx, signature) +} + +// UpdateSignature updates the specified signature +func (s service) UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error { + return s.repo.UpdateSignature(ctx, signatureID, updates) +} + // GetIndividualSignature returns the signature associated with the specified CLA Group and User ID -func (s service) GetIndividualSignature(ctx context.Context, claGroupID, userID string) (*models.Signature, error) { - return s.repo.GetIndividualSignature(ctx, claGroupID, userID) +func (s service) GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) { + return s.repo.GetIndividualSignature(ctx, claGroupID, userID, approved, signed) +} + +// GetIndividualSignatures returns the list of signatures associated with the specified CLA Group and User ID +func (s service) GetIndividualSignatures(ctx context.Context, claGroupID, userID string, approved, signed *bool) ([]*models.Signature, error) { + return s.repo.GetIndividualSignatures(ctx, claGroupID, userID, approved, signed) } // GetCorporateSignature returns the signature associated with the specified CLA Group and Company ID -func (s service) GetCorporateSignature(ctx context.Context, claGroupID, companyID string) (*models.Signature, error) { - return s.repo.GetCorporateSignature(ctx, claGroupID, companyID) +func (s service) GetCorporateSignature(ctx context.Context, claGroupID, companyID string, approved, signed *bool) (*models.Signature, error) { + return s.repo.GetCorporateSignature(ctx, claGroupID, companyID, approved, signed) +} + +// GetCorporateSignatures returns the list of signature associated with the specified CLA Group and Company ID +func (s service) GetCorporateSignatures(ctx context.Context, claGroupID, companyID string, approved, signed *bool) ([]*models.Signature, error) { + return s.repo.GetCorporateSignatures(ctx, claGroupID, companyID, approved, signed) } // GetProjectSignatures returns the list of signatures associated with the specified project func (s service) GetProjectSignatures(ctx context.Context, params signatures.GetProjectSignaturesParams) (*models.Signatures, error) { - const defaultPageSize int64 = 10 - var pageSize = defaultPageSize - if params.PageSize != nil { - pageSize = *params.PageSize + projectSignatures, err := s.repo.GetProjectSignatures(ctx, params) + if err != nil { + return nil, err } - projectSignatures, err := s.repo.GetProjectSignatures(ctx, params, pageSize) + return projectSignatures, nil +} + +// CreateOrUpdateEmployeeSignature creates or updates the specified signature +func (s service) UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error) { + return s.repo.UpdateEnvelopeDetails(ctx, signatureID, envelopeID, signURL) +} + +// CreateProjectSummaryReport generates a project summary report based on the specified input +func (s service) CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) { + + projectSignatures, err := s.repo.CreateProjectSummaryReport(ctx, params) if err != nil { return nil, err } @@ -109,8 +176,13 @@ func (s service) GetProjectSignatures(ctx context.Context, params signatures.Get } // GetProjectCompanySignature returns the signature associated with the specified project and company -func (s service) GetProjectCompanySignature(ctx context.Context, companyID, projectID string, signed, approved *bool, nextKey *string, pageSize *int64) (*models.Signature, error) { - return s.repo.GetProjectCompanySignature(ctx, companyID, projectID, signed, approved, nextKey, pageSize) +func (s service) GetProjectCompanySignature(ctx context.Context, companyID, projectID string, approved, signed *bool, nextKey *string, pageSize *int64) (*models.Signature, error) { + return s.repo.GetProjectCompanySignature(ctx, companyID, projectID, approved, signed, nextKey, pageSize) +} + +// CreateIndividualSignature creates a new individual signature +func (s service) CreateSignature(ctx context.Context, signature *ItemSignature) error { + return s.repo.CreateSignature(ctx, signature) } // GetProjectCompanySignatures returns the list of signatures associated with the specified project @@ -135,15 +207,13 @@ func (s service) GetProjectCompanySignatures(ctx context.Context, params signatu } // GetProjectCompanyEmployeeSignatures returns the list of employee signatures associated with the specified project -func (s service) GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams) (*models.Signatures, error) { +func (s service) GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *ApprovalCriteria) (*models.Signatures, error) { - const defaultPageSize int64 = 10 - var pageSize = defaultPageSize - if params.PageSize != nil { - pageSize = *params.PageSize + if params.PageSize == nil { + params.PageSize = utils.Int64(10) } - projectSignatures, err := s.repo.GetProjectCompanyEmployeeSignatures(ctx, params, pageSize) + projectSignatures, err := s.repo.GetProjectCompanyEmployeeSignatures(ctx, params, criteria) if err != nil { return nil, err } @@ -173,8 +243,13 @@ func (s service) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, return s.repo.GetCompanyIDsWithSignedCorporateSignatures(ctx, claGroupID) } +// GetCCLASignatures returns the list of CCLA signatures +func (s service) GetCCLASignatures(ctx context.Context, signed, approved *bool) ([]*ItemSignature, error) { + return s.repo.GetCCLASignatures(ctx, signed, approved) +} + // GetUserSignatures returns the list of user signatures associated with the specified user -func (s service) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams) (*models.Signatures, error) { +func (s service) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, projectID *string) (*models.Signatures, error) { const defaultPageSize int64 = 10 var pageSize = defaultPageSize @@ -182,7 +257,7 @@ func (s service) GetUserSignatures(ctx context.Context, params signatures.GetUse pageSize = *params.PageSize } - userSignatures, err := s.repo.GetUserSignatures(ctx, params, pageSize) + userSignatures, err := s.repo.GetUserSignatures(ctx, params, pageSize, projectID) if err != nil { return nil, err } @@ -190,18 +265,18 @@ func (s service) GetUserSignatures(ctx context.Context, params signatures.GetUse return userSignatures, nil } -// GetGithubOrganizationsFromWhitelist retrieves the organization from the whitelist -func (s service) GetGithubOrganizationsFromWhitelist(ctx context.Context, signatureID string, githubAccessToken string) ([]models.GithubOrg, error) { +// GetGithubOrganizationsFromApprovalList retrieves the organization from the approval list +func (s service) GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string, githubAccessToken string) ([]models.GithubOrg, error) { if signatureID == "" { - msg := "unable to get GitHub organizations whitelist - signature ID is nil" + msg := "unable to get GitHub organizations approval list - signature ID is nil" log.Warn(msg) return nil, errors.New(msg) } - orgIds, err := s.repo.GetGithubOrganizationsFromWhitelist(ctx, signatureID) + orgIds, err := s.repo.GetGithubOrganizationsFromApprovalList(ctx, signatureID) if err != nil { - log.Warnf("error loading github organization from whitelist using signatureID: %s, error: %v", + log.Warnf("error loading github organization from approval list using signatureID: %s, error: %v", signatureID, err) return nil, err } @@ -243,18 +318,18 @@ func (s service) GetGithubOrganizationsFromWhitelist(ctx context.Context, signat return orgIds, nil } -// AddGithubOrganizationToWhitelist adds the GH organization to the whitelist -func (s service) AddGithubOrganizationToWhitelist(ctx context.Context, signatureID string, whiteListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) { - organizationID := whiteListParams.OrganizationID +// AddGithubOrganizationToApprovalList adds the GH organization to the approval list +func (s service) AddGithubOrganizationToApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) { + organizationID := approvalListParams.OrganizationID if signatureID == "" { - msg := "unable to add GitHub organization from whitelist - signature ID is nil" + msg := "unable to add GitHub organization from approval list - signature ID is nil" log.Warn(msg) return nil, errors.New(msg) } if organizationID == nil { - msg := "unable to add GitHub organization from whitelist - organization ID is nil" + msg := "unable to add GitHub organization from approval list - organization ID is nil" log.Warn(msg) return nil, errors.New(msg) } @@ -303,30 +378,30 @@ func (s service) AddGithubOrganizationToWhitelist(ctx context.Context, signature } } - gitHubWhiteList, err := s.repo.AddGithubOrganizationToWhitelist(ctx, signatureID, *organizationID) + gitHubOrgApprovalList, err := s.repo.AddGithubOrganizationToApprovalList(ctx, signatureID, *organizationID) if err != nil { - log.Warnf("issue adding github organization to white list using signatureID: %s, gh org id: %s, error: %v", + log.Warnf("issue adding github organization to approval list using signatureID: %s, gh org id: %s, error: %v", signatureID, *organizationID, err) return nil, err } - return gitHubWhiteList, nil + return gitHubOrgApprovalList, nil } -// DeleteGithubOrganizationFromWhitelist deletes the specified GH organization from the whitelist -func (s service) DeleteGithubOrganizationFromWhitelist(ctx context.Context, signatureID string, whiteListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) { +// DeleteGithubOrganizationFromApprovalList deletes the specified GH organization from the approval list +func (s service) DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) { // Extract the payload values - organizationID := whiteListParams.OrganizationID + organizationID := approvalListParams.OrganizationID if signatureID == "" { - msg := "unable to delete GitHub organization from whitelist - signature ID is nil" + msg := "unable to delete GitHub organization from approval list - signature ID is nil" log.Warn(msg) return nil, errors.New(msg) } if organizationID == nil { - msg := "unable to delete GitHub organization from whitelist - organization ID is nil" + msg := "unable to delete GitHub organization from approval list - organization ID is nil" log.Warn(msg) return nil, errors.New(msg) } @@ -375,34 +450,49 @@ func (s service) DeleteGithubOrganizationFromWhitelist(ctx context.Context, sign } } - gitHubWhiteList, err := s.repo.DeleteGithubOrganizationFromWhitelist(ctx, signatureID, *organizationID) + gitHubOrgApprovalList, err := s.repo.DeleteGithubOrganizationFromApprovalList(ctx, signatureID, *organizationID) if err != nil { return nil, err } - return gitHubWhiteList, nil + return gitHubOrgApprovalList, nil } -// UpdateApprovalList service method -func (s service) UpdateApprovalList(ctx context.Context, authUser *auth.User, claGroupModel *models.ClaGroup, companyModel *models.Company, claGroupID string, params *models.ApprovalList) (*models.Signature, error) { +// UpdateApprovalList service method which handles updating the various approval lists +func (s service) UpdateApprovalList(ctx context.Context, authUser *auth.User, claGroupModel *models.ClaGroup, companyModel *models.Company, claGroupID string, params *models.ApprovalList, projectSFID string) (*models.Signature, error) { // nolint gocyclo + f := logrus.Fields{ + "functionName": "v1.signatures.service.UpdateApprovalList", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser.UserName": authUser.UserName, + "authUser.Email": authUser.Email, + "claGroupID": claGroupID, + "claGroupName": claGroupModel.ProjectName, + "companyName": companyModel.CompanyName, + "companyID": companyModel.CompanyID, + } + + log.WithFields(f).Debugf("processing update approval list request") + + // Lookup the project corporate signature - should have one pageSize := int64(1) signed, approved := true, true - sigModel, sigErr := s.GetProjectCompanySignature(ctx, companyModel.CompanyID, claGroupID, &signed, &approved, nil, &pageSize) + corporateSigModel, sigErr := s.GetProjectCompanySignature(ctx, companyModel.CompanyID, claGroupID, &signed, &approved, nil, &pageSize) if sigErr != nil { msg := fmt.Sprintf("unable to locate project company signature by Company ID: %s, Project ID: %s, CLA Group ID: %s, error: %+v", companyModel.CompanyID, claGroupModel.ProjectID, claGroupID, sigErr) - log.Warn(msg) + log.WithFields(f).WithError(sigErr).Warn(msg) return nil, NewBadRequestError(msg) } - if sigModel == nil { + // If not found, return error + if corporateSigModel == nil { msg := fmt.Sprintf("unable to locate signature for company ID: %s CLA Group ID: %s, type: ccla, signed: %t, approved: %t", companyModel.CompanyID, claGroupID, signed, approved) - log.Warn(msg) + log.WithFields(f).Warn(msg) return nil, NewBadRequestError(msg) } // Ensure current user is in the Signature ACL - claManagers := sigModel.SignatureACL + claManagers := corporateSigModel.SignatureACL if !utils.CurrentUserInACL(authUser, 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", authUser.UserName, authUser.Email, @@ -411,42 +501,409 @@ func (s service) UpdateApprovalList(ctx context.Context, authUser *auth.User, cl return nil, NewForbiddenError(msg) } - // Lookup the user making the request + // Lookup the user making the request - should be the CLA Manager userModel, userErr := s.usersService.GetUserByUserName(authUser.UserName, true) if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup CLA Manager user by user name: %s", authUser.UserName) return nil, userErr } - updatedSig, err := s.repo.UpdateApprovalList(ctx, claGroupModel.ProjectID, companyModel.CompanyID, params) + // This event is ONLY used when we need to invalidate the signature + eventArgs := &events.LogEventArgs{ + EventType: events.InvalidatedSignature, // reviewed and + ProjectID: claGroupModel.ProjectExternalID, + ClaGroupModel: claGroupModel, + CompanyID: companyModel.CompanyID, + CompanyModel: companyModel, + LfUsername: userModel.LfUsername, + UserID: userModel.UserID, + UserModel: userModel, + ProjectSFID: projectSFID, + } + + updatedCorporateSignature, err := s.repo.UpdateApprovalList(ctx, userModel, claGroupModel, companyModel.CompanyID, params, eventArgs) if err != nil { - return updatedSig, err + log.WithFields(f).WithError(err).Warnf("problem updating approval list for company ID: %s, project ID: %s, cla group ID: %s", companyModel.CompanyID, claGroupModel.ProjectID, claGroupID) + return updatedCorporateSignature, err + } + + // If auto create ECLA is enabled for this Corporate Agreement, then create an ECLA for each employee that was added to the approval list + // we get the complete user list as output from the processing of the approval list + var userModelList []*models.User + if corporateSigModel.AutoCreateECLA { + log.WithFields(f).Debug("auto-create ECLA option is enabled - processing auto-enable request for all items on the approval list...") + userList, processErr := s.CreateOrUpdateEmployeeSignature(ctx, claGroupModel, companyModel, updatedCorporateSignature) + if processErr != nil { + log.WithFields(f).WithError(processErr).Warnf("problem processing auto-enable request for company ID: %s, project ID: %s, cla group ID: %s", companyModel.CompanyID, claGroupModel.ProjectID, claGroupID) + } + userModelList = userList + } else { + userList, processErr := s.createOrGetEmployeeModels(ctx, claGroupModel, companyModel, updatedCorporateSignature) + if processErr != nil { + log.WithFields(f).WithError(processErr).Warnf("problem processing user list for company ID: %s, project ID: %s, cla group ID: %s", companyModel.CompanyID, claGroupModel.ProjectID, claGroupID) + } + userModelList = userList } - // Log Events - s.createEventLogEntries(companyModel, claGroupModel, userModel, params) + var wg sync.WaitGroup + + // Log Events that the CLA manager updated the approval lists - do it in a separate go routine + log.WithFields(f).Debugf("creating event log entry...") + wg.Add(1) + go func() { + defer wg.Done() + s.createEventLogEntries(ctx, companyModel, claGroupModel, userModel, params, projectSFID) + }() - // Send an email to the CLA Managers + // Send an email to each of the CLA Managers - do it in a separate go routine + log.WithFields(f).Debugf("sending notification email to cla managers...") for _, claManager := range claManagers { - claManagerEmail := getBestEmail(claManager) - s.sendApprovalListUpdateEmailToCLAManagers(companyModel, claGroupModel, claManager.Username, claManagerEmail, params) + wg.Add(1) + go func(companyModel *models.Company, claGroupModel *models.ClaGroup, claManager models.User, params *models.ApprovalList) { + defer wg.Done() + claManagerEmail := getBestEmail(&claManager) // nolint + s.sendApprovalListUpdateEmailToCLAManagers(companyModel, claGroupModel, claManager.Username, claManagerEmail, params) + }(companyModel, claGroupModel, claManager, params) + } + + // Send emails to contributors if email or GitHub/GitLab username was added or removed - do it in a separate go routine + log.WithFields(f).Debugf("sending notification email to contributors...") + wg.Add(1) + go func() { + defer wg.Done() + s.sendRequestAccessEmailToContributors(authUser, companyModel, claGroupModel, params) + }() + + // For each employee that was added, update their GitHub PRs - they are now on the approval list (and if auto-acknowledge is enabled, they are also approved) + // do this in a separate go routine + for _, employeeUserModel := range userModelList { + wg.Add(1) + // Update the GitHub status for the employee in the background + go func(ctx context.Context, employeeUserModel *models.User) { + defer wg.Done() + handleStatusErr := s.handleGitHubStatusUpdate(ctx, employeeUserModel) + if handleStatusErr != nil { + log.WithFields(f).WithError(handleStatusErr).Warnf("problem updating GitHub status for user: %+v", employeeUserModel) + } + }(utils.NewContextFromParent(ctx), employeeUserModel) + } + + // Wait until all the go routines are done - if we don't wait, the behavior is undefined + wg.Wait() + + return updatedCorporateSignature, nil +} + +func (s service) createOrGetEmployeeModels(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) { // nolint gocyclomatic + f := logrus.Fields{ + "functionName": "v2.company.service.createOrGetEmployeeModels", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupModel.ProjectID, + "claGroupName": claGroupModel.ProjectName, + "companyName": companyModel.CompanyName, + "companyID": companyModel.CompanyID, + } + var employeeUserModel *models.User + var userLookupErr error + + log.WithFields(f).Debugf("processing %d approval list entries", len(corporateSignatureModel.EmailApprovalList)+len(corporateSignatureModel.GithubUsernameApprovalList)+len(corporateSignatureModel.GitlabUsernameApprovalList)) + + // Most of the following business logic is all the same - however, we need to handle the different types of approval lists entries and process them in the same way + // We build a list of users to process - this is a list of simple user models that contain the email, GitHub username, and GitLab username - typically only one of the values in the model will be set + //userList := make([]simpleUserInfoModel, len(corporateSignatureModel.EmailApprovalList)+len(corporateSignatureModel.GithubUsernameApprovalList)+len(corporateSignatureModel.GitlabUsernameApprovalList)) + var userList []simpleUserInfoModel + for _, email := range corporateSignatureModel.EmailApprovalList { + log.WithFields(f).Debugf("adding email: %s", email) + userList = append(userList, simpleUserInfoModel{ + Email: email, + }) + } + for _, gitHubUserName := range corporateSignatureModel.GithubUsernameApprovalList { + log.WithFields(f).Debugf("adding GitHub username: %s", gitHubUserName) + userList = append(userList, simpleUserInfoModel{ + GitHubUserName: gitHubUserName, + }) + } + for _, gitLabUserName := range corporateSignatureModel.GitlabUsernameApprovalList { + log.WithFields(f).Debugf("adding GitLab username: %s", gitLabUserName) + userList = append(userList, simpleUserInfoModel{ + GitLabUserName: gitLabUserName, + }) + } + + // employeeUserModels := make([]*models.User, len(corporateSignatureModel.EmailApprovalList)+len(corporateSignatureModel.GithubUsernameApprovalList)+len(corporateSignatureModel.GitlabUsernameApprovalList)) + var employeeUserModels []*models.User + var responseErr error + + // For each item in the email approval list... + for _, simpleUserInfoModelEntry := range userList { + log.WithFields(f).Debugf("processing approval list entry: %+v", simpleUserInfoModelEntry) + + // Grab the current time + _, currentTime := utils.CurrentTime() + + if simpleUserInfoModelEntry.Email != "" { + employeeUserModel, userLookupErr = s.usersService.GetUserByEmail(simpleUserInfoModelEntry.Email) + if userLookupErr == nil && employeeUserModel != nil { + updatedEmployeeUserModel, updateErr := s.updateUserCompanyID(ctx, employeeUserModel, companyModel) + if updatedEmployeeUserModel != nil && updateErr == nil { + // Use the updated user model + employeeUserModel = updatedEmployeeUserModel + } + if updatedEmployeeUserModel != nil && updateErr == nil { + employeeUserModel = updatedEmployeeUserModel + } + employeeUserModels = append(employeeUserModels, employeeUserModel) + + continue + } + } + + if simpleUserInfoModelEntry.GitHubUserName != "" { + employeeUserModel, userLookupErr = s.usersService.GetUserByGitHubUsername(simpleUserInfoModelEntry.GitHubUserName) + if userLookupErr != nil { + log.WithFields(f).WithError(userLookupErr).Warnf("problem looking up user by GitHub username: %s", simpleUserInfoModelEntry.GitHubUserName) + } else if userLookupErr == nil && employeeUserModel != nil { + updatedEmployeeUserModel, updateErr := s.updateUserCompanyID(ctx, employeeUserModel, companyModel) + if updatedEmployeeUserModel != nil && updateErr == nil { + // Use the updated user model + employeeUserModel = updatedEmployeeUserModel + } + employeeUserModels = append(employeeUserModels, employeeUserModel) + + continue + } + + // Additional lookup logic - use the GitHub API to grab additional user information + if employeeUserModel == nil { + // Need more information before we can create a new user record - attempt to locate the GitHub user + // record by the GitHub username - we need the GitHub numeric ID value which was not provided by the UI/API call + gitHubUserModel, gitHubErr := github.GetUserDetails(simpleUserInfoModelEntry.GitHubUserName) + // Should get a model, no errors and have at least the ID + if gitHubErr != nil || gitHubUserModel == nil || gitHubUserModel.ID == nil { + log.WithFields(f).WithError(gitHubErr).Warnf("problem looking up GitHub user details for user: %s, model: %+v, error: %+v", simpleUserInfoModelEntry.GitHubUserName, gitHubUserModel, gitHubErr) + responseErr = gitHubErr + continue + } + + if gitHubUserModel.ID != nil { + simpleUserInfoModelEntry.GitHubUserID = strconv.FormatInt(*gitHubUserModel.ID, 10) + } + // User may not have a public email + if gitHubUserModel.Email != nil { + simpleUserInfoModelEntry.Email = *gitHubUserModel.Email + } + } + } + + if simpleUserInfoModelEntry.GitLabUserName != "" { + employeeUserModel, userLookupErr = s.usersService.GetUserByGitLabUsername(simpleUserInfoModelEntry.GitLabUserName) + if userLookupErr != nil { + log.WithFields(f).WithError(userLookupErr).Warnf("problem looking up user by GitLab username: %s", simpleUserInfoModelEntry.GitHubUserName) + } else if userLookupErr == nil && employeeUserModel != nil { + updatedEmployeeUserModel, updateErr := s.updateUserCompanyID(ctx, employeeUserModel, companyModel) + if updatedEmployeeUserModel != nil && updateErr == nil { + // Use the updated user model + employeeUserModel = updatedEmployeeUserModel + } + employeeUserModels = append(employeeUserModels, employeeUserModel) + + continue + } + + // Additional lookup logic - use the GitLab API to grab additional user information + if employeeUserModel == nil { + // Harold - this bit of logic needs finishing/review/testing + // Take the CLA Group ID and look into the GitLab Orgs table and fine one of the GitLab Project/Org records + // From one of records, we need to decode the access token and use that to create a GitLab client + // This will give us the accessInfo we need to create the GitLab client + accessInfo := "" // TODO: Need to get the access token from one of the exising GitLab repositories ? + gitLabClient, gitLabClientErr := gitlab_api.NewGitlabOauthClient(accessInfo, s.gitLabApp) + if gitLabClientErr != nil { + log.WithFields(f).WithError(gitLabClientErr).Warnf("problem creating GitLab client for user: %s, error: %+v", simpleUserInfoModelEntry.GitLabUserName, gitLabClientErr) + responseErr = gitLabClientErr + continue + } + + // Attempt to lookup the GitLab user record by the GitLab username - we need the GitLab numeric ID value which was not provided by the UI/API call + gitLabUserModel, gitLabErr := gitlab_api.GetUserByName(ctx, gitLabClient, simpleUserInfoModelEntry.GitLabUserName) + // Should get a model, no errors and have at least the ID + if gitLabErr != nil || gitLabUserModel == nil || gitLabUserModel.ID == 0 { + log.WithFields(f).WithError(gitLabErr).Warnf("problem looking up GitLab user details for user: %s, model: %+v, error: %+v", simpleUserInfoModelEntry.GitLabUserName, gitLabUserModel, gitLabErr) + responseErr = gitLabErr + continue + } + + if gitLabUserModel.ID != 0 { + simpleUserInfoModelEntry.GitHubUserID = strconv.FormatInt(int64(gitLabUserModel.ID), 10) + } + // User may not have a public email + if gitLabUserModel.Email != "" { + simpleUserInfoModelEntry.Email = gitLabUserModel.Email + } + } + } + + // If we couldn't find the user, then create a user record + if employeeUserModel == nil { + log.WithFields(f).WithError(userLookupErr).Debugf("unable to lookup existing user by one of the values: %+v", simpleUserInfoModelEntry) + var userCreateErr error + + // Create a new user record based on the email and company ID + employeeUserModel, userCreateErr = s.createUserModel( + simpleUserInfoModelEntry.GitHubUserName, simpleUserInfoModelEntry.GitHubUserID, + simpleUserInfoModelEntry.GitLabUserName, simpleUserInfoModelEntry.GitLabUserID, + simpleUserInfoModelEntry.Email, companyModel.CompanyID, + fmt.Sprintf("auto generated ECLA user from CLA Manager adding user to the approval list with auto_create_ecla feature flag set to true on %+v.", currentTime)) + if userCreateErr != nil || employeeUserModel == nil { + log.WithFields(f).WithError(userCreateErr).Warnf("unable to create a new user by one of the values: %+v", simpleUserInfoModelEntry) + continue + } + + employeeUserModels = append(employeeUserModels, employeeUserModel) + log.WithFields(f).Debugf("created user using: %+v with company ID: %s", simpleUserInfoModelEntry, companyModel.CompanyID) + } + } + + return employeeUserModels, responseErr +} + +func (s service) updateUserCompanyID(ctx context.Context, employeeUserModel *models.User, companyModel *models.Company) (*models.User, error) { + f := logrus.Fields{ + "functionName": "v2.company.service.updateUserCompanyID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": companyModel.CompanyName, + "companyID": companyModel.CompanyID, + } + + if employeeUserModel.CompanyID == "" || employeeUserModel.CompanyID != companyModel.CompanyID { + _, currentTime := utils.CurrentTime() + + log.WithFields(f).Debugf("updating user record - set company ID = %s - previous value was: %s", companyModel.CompanyID, employeeUserModel.CompanyID) + employeeUserModel.CompanyID = companyModel.CompanyID + userUpdateErr := s.usersService.UpdateUserCompanyID( + employeeUserModel.UserID, + companyModel.CompanyID, + fmt.Sprintf("auto assign companyID from CLA Manager adding user to the company approval list with auto_create_ecla feature flag set to true on %+v.", currentTime)) + if userUpdateErr != nil { + log.WithFields(f).WithError(userUpdateErr).Warnf("problem updating user record with company ID: %s", companyModel.CompanyID) + return nil, userUpdateErr + } + + log.WithFields(f).Debugf("updated user record with company ID: %s", companyModel.CompanyID) + // Reload and return the updated user model + return s.usersService.GetUser(employeeUserModel.UserID) } - // Send emails to contributors if email or GH username as added/removed - s.sendRequestAccessEmailToContributors(authUser, companyModel, claGroupModel, params) + return employeeUserModel, nil +} + +// CreateOrUpdateEmployeeSignature creates or updates the employee signature for the given company +func (s service) CreateOrUpdateEmployeeSignature(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) { + f := logrus.Fields{ + "functionName": "v2.company.service.CreateOrUpdateEmployeeSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupModel.ProjectID, + "claGroupName": claGroupModel.ProjectName, + "companyName": companyModel.CompanyName, + "companyID": companyModel.CompanyID, + } + + // Most of the following business logic is all the same - however, we need to handle the different types of approval lists entries and process them in the same way + // We build a list of users to process - this is a list of simple user models that contain the email, GitHub username, and GitLab username - typically only one of the values in the model will be set + userList, userErr := s.createOrGetEmployeeModels(ctx, claGroupModel, companyModel, corporateSignatureModel) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("problem creating or loading user records from the approval list") + } + + responseErr := s.processEmployeeSignatures(ctx, companyModel, claGroupModel, userList) + + if responseErr != nil { + log.WithFields(f).WithError(responseErr).Warnf("problem processing employee signatures") + } + + return userList, responseErr +} + +func (s service) processEmployeeSignatures(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, userList []*models.User) error { + f := logrus.Fields{ + "functionName": "v2.company.service.processEmployeeSignatures", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupModel.ProjectID, + "companyName": companyModel.CompanyName, + "companyID": companyModel.CompanyID, + } + + var responseErr error + var wg sync.WaitGroup + resultChan := make(chan *EmployeeModel) + errChan := make(chan error) + + // For each item in the email approval list... + for _, employeeUserModel := range userList { + wg.Add(1) + go s.repo.GetProjectCompanyEmployeeSignature(ctx, companyModel, claGroupModel, employeeUserModel, &wg, resultChan, errChan) + } + + // Wait for all the go routines to complete + go func() { + wg.Wait() + close(resultChan) + close(errChan) + }() + + for employeeModel := range resultChan { + if employeeModel != nil { + employeeSignatureModel := employeeModel.Signature + employeeUserModel := employeeModel.User + log.WithFields(f).Debugf("processing employee signature record for user: %+s", employeeUserModel.UserID) + if employeeSignatureModel != nil { + if !employeeSignatureModel.SignatureApproved || !employeeSignatureModel.SignatureSigned { + // If record exists, this will update the record + log.WithFields(f).Debugf("updating employee signature record for: %+v", employeeSignatureModel) + updateErr := s.repo.ValidateProjectRecord(ctx, employeeSignatureModel.SignatureID, "signed and approved employee acknowledgement since auto_create_ecla feature flag set to true") + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("problem updating employee signature record for: %+v", employeeSignatureModel) + responseErr = updateErr + } + } else { + log.WithFields(f).Debugf("employee signature record already exists for: %+v", employeeUserModel) + } + } else { + // Ok, auto-create the employee acknowledgement record + log.WithFields(f).Debugf("creating employee signature record for user: %+s", employeeUserModel.UserID) + createErr := s.repo.CreateProjectCompanyEmployeeSignature(ctx, companyModel, claGroupModel, employeeUserModel) + if createErr != nil { + log.WithFields(f).WithError(createErr).Warnf("unable to create project company employee signature record for: %+v", employeeUserModel) + responseErr = createErr + } + } + } + } + + // Check for any errors + for err := range errChan { + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem looking up employee signature record ") + responseErr = err + } + } - return updatedSig, nil + log.WithFields(f).Debugf("completed processing employee signatures") + + return responseErr } -// Disassociate project signatures -func (s service) InvalidateProjectRecords(ctx context.Context, projectID string, projectName string) (int, error) { +// InvalidateProjectRecords disassociates project signatures +func (s service) InvalidateProjectRecords(ctx context.Context, projectID, note string) (int, error) { f := logrus.Fields{ - "functionName": "InvalidateProjectRecords", + "functionName": "v1.signatures.service.InvalidateProjectRecords", "projectID": projectID, - "projectName": projectName} + } result, err := s.repo.ProjectSignatures(ctx, projectID) if err != nil { - log.WithFields(f).Warnf(fmt.Sprintf("Unable to get signatures for project: %s", projectID)) + log.WithFields(f).WithError(err).Warnf(fmt.Sprintf("Unable to get signatures for project: %s", projectID)) return 0, err } @@ -457,14 +914,13 @@ func (s service) InvalidateProjectRecords(ctx context.Context, projectID string, len(result.Signatures), projectID)) for _, signature := range result.Signatures { // Do this in parallel, as we could have a lot to invalidate - go func(sigID, projName string) { + go func(sigID, projectID string) { defer wg.Done() - updateErr := s.repo.InvalidateProjectRecord(ctx, sigID, projName) + updateErr := s.repo.InvalidateProjectRecord(ctx, sigID, note) if updateErr != nil { - log.WithFields(f).Warnf("Unable to update signature: %s with project name: %s, error: %v", - sigID, projName, updateErr) + log.WithFields(f).WithError(updateErr).Warnf("Unable to update signature: %s with project ID: %s, error: %v", sigID, projectID, updateErr) } - }(signature.SignatureID.String(), projectName) + }(signature.SignatureID, projectID) } // Wait until all the workers are done @@ -504,346 +960,499 @@ func buildApprovalListSummary(approvalListChanges *models.ApprovalList) string { approvalListSummary += appendList(approvalListChanges.RemoveEmailApprovalList, "Removed Email:") approvalListSummary += appendList(approvalListChanges.AddDomainApprovalList, "Added Domain:") approvalListSummary += appendList(approvalListChanges.RemoveDomainApprovalList, "Removed Domain:") - approvalListSummary += appendList(approvalListChanges.AddGithubUsernameApprovalList, "Added GithHub User:") + approvalListSummary += appendList(approvalListChanges.AddGithubUsernameApprovalList, "Added GitHub User:") approvalListSummary += appendList(approvalListChanges.RemoveGithubUsernameApprovalList, "Removed GitHub User:") - approvalListSummary += appendList(approvalListChanges.AddGithubOrgApprovalList, "Added GithHub Organization:") + approvalListSummary += appendList(approvalListChanges.AddGithubOrgApprovalList, "Added GitHub Organization:") approvalListSummary += appendList(approvalListChanges.RemoveGithubOrgApprovalList, "Removed GitHub Organization:") + approvalListSummary += appendList(approvalListChanges.AddGitlabUsernameApprovalList, "Added Gitlab User:") + approvalListSummary += appendList(approvalListChanges.RemoveGitlabUsernameApprovalList, "Removed Gitlab User:") + approvalListSummary += appendList(approvalListChanges.AddGitlabOrgApprovalList, "Added Gitlab Organization:") + approvalListSummary += appendList(approvalListChanges.RemoveGitlabOrgApprovalList, "Removed Gitlab Organization:") approvalListSummary += "" return approvalListSummary } -// 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) { +func (s service) GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) { + return s.repo.GetClaGroupICLASignatures(ctx, claGroupID, searchTerm, approved, signed, pageSize, nextKey, withExtraDetails) +} + +func (s service) GetClaGroupCCLASignatures(ctx context.Context, claGroupID string, approved, signed *bool) (*models.Signatures, error) { + pageSize := utils.Int64(1000) + return s.repo.GetProjectSignatures(ctx, signatures.GetProjectSignaturesParams{ + ClaType: aws.String(utils.ClaTypeCCLA), + ProjectID: claGroupID, + PageSize: pageSize, + Approved: approved, + Signed: signed, + }) +} + +func (s service) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey *string, searchTerm *string) (*models.CorporateContributorList, error) { + return s.repo.GetClaGroupCorporateContributors(ctx, claGroupID, companyID, pageSize, nextKey, searchTerm) +} + +// updateChangeRequest is a helper function that updates PR - typically after the auto ecla update +func (s service) updateChangeRequest(ctx context.Context, ghOrg *models.GithubOrganization, repositoryID, pullRequestID int64, projectID string) error { 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 { - 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, "
  • mgr_one mgr_one_email
  • ") + +} diff --git a/cla-backend-go/tests/utils_context_test.go b/cla-backend-go/tests/utils_context_test.go new file mode 100644 index 000000000..70f4c864d --- /dev/null +++ b/cla-backend-go/tests/utils_context_test.go @@ -0,0 +1,38 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "context" + "testing" + + "github.com/LF-Engineering/lfx-kit/auth" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +// TestGetUserNameFromContext is a test for the GetUserNameFromContext +func TestGetUserNameFromContext(t *testing.T) { + reqID := "foo123" + authUser := &auth.User{ + UserName: "ddeal1", + Email: "ddeal1@foo.com", + ACL: auth.ACL{}, + } + ctx := utils.ContextWithRequestAndUser(context.Background(), reqID, authUser) // nolint + assert.Equal(t, "ddeal1", utils.GetUserNameFromContext(ctx)) +} + +// TestGetUserEmailFromContext is a test for the GetUserNameFromContext +func TestGetUserEmailFromContext(t *testing.T) { + reqID := "foo566" + authUser := &auth.User{ + UserName: "ddeal2", + Email: "ddeal2@foo.com", + ACL: auth.ACL{}, + } + ctx := utils.ContextWithRequestAndUser(context.Background(), reqID, authUser) // nolint + assert.Equal(t, "ddeal2@foo.com", utils.GetUserEmailFromContext(ctx)) +} diff --git a/cla-backend-go/tests/utils_conversion_test.go b/cla-backend-go/tests/utils_conversion_test.go new file mode 100644 index 000000000..e18cd79a3 --- /dev/null +++ b/cla-backend-go/tests/utils_conversion_test.go @@ -0,0 +1,28 @@ +// 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/stretchr/testify/assert" +) + +// TestValidCompanyName is a test for the GetNilSliceIfEmpty +func TestGetNilSliceIfEmptyWithData(t *testing.T) { + slice := []string{"dog", "cat"} + assert.Equal(t, []string{"dog", "cat"}, utils.GetNilSliceIfEmpty(slice), "GetNilSliceIfEmpty - With Data") +} + +// TestGetNilSliceIfEmptyWithEmptySlice is a test for the GetNilSliceIfEmpty +func TestGetNilSliceIfEmptyWithEmptySlice(t *testing.T) { + var slice []string + assert.Nil(t, utils.GetNilSliceIfEmpty(slice), "GetNilSliceIfEmpty - Empty Slice") +} + +// TestGetNilSliceIfEmptyWithNil is a test for the GetNilSliceIfEmpty +func TestGetNilSliceIfEmptyWithNil(t *testing.T) { + assert.Nil(t, utils.GetNilSliceIfEmpty(nil), "GetNilSliceIfEmpty - Nil") +} diff --git a/cla-backend-go/tests/utils_list_utils_test.go b/cla-backend-go/tests/utils_list_utils_test.go new file mode 100644 index 000000000..e6d5f1e08 --- /dev/null +++ b/cla-backend-go/tests/utils_list_utils_test.go @@ -0,0 +1,38 @@ +// 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/stretchr/testify/assert" +) + +func TestFindInt64Duplicates(t *testing.T) { + type TestCase struct { + A []int64 + B []int64 + Expected []int64 + } + testInputs := []TestCase{ + {nil, nil, []int64{}}, + {nil, []int64{}, []int64{}}, + {[]int64{}, nil, []int64{}}, + {nil, nil, []int64{}}, + {[]int64{}, []int64{}, []int64{}}, + {[]int64{1}, []int64{}, []int64{}}, + {[]int64{}, []int64{1}, []int64{}}, + {[]int64{1, 2}, []int64{}, []int64{}}, + {[]int64{}, []int64{1, 2}, []int64{}}, + {[]int64{1}, []int64{1}, []int64{1}}, + {[]int64{1, 2}, []int64{1}, []int64{1}}, + {[]int64{1, 2}, []int64{1, 3, 4}, []int64{1}}, + {[]int64{1, 2, 3, 4, 5}, []int64{1, 5, 3, 4}, []int64{1, 3, 5, 4}}, + } + + for _, testInput := range testInputs { + assert.ElementsMatch(t, testInput.Expected, utils.FindInt64Duplicates(testInput.A, testInput.B)) + } +} diff --git a/cla-backend-go/tests/utils_string_utils_test.go b/cla-backend-go/tests/utils_string_utils_test.go index f612c0427..c55173964 100644 --- a/cla-backend-go/tests/utils_string_utils_test.go +++ b/cla-backend-go/tests/utils_string_utils_test.go @@ -38,3 +38,31 @@ func TestTrimSpaceFromItems(t *testing.T) { assert.ObjectsAreEqualValues(expectedResults[i], utils.TrimSpaceFromItems(testInputs[i])) } } + +func TestGetFirstAndLastName(t *testing.T) { + + testInputs := []string{ + "", + "John", + "John Smith", + "John Smith", + "John Harold Smith", + "John Harold Smith", + "John Harold Zeek Smith", + } + expectedResults := [][]string{ + {"", ""}, + {"John", ""}, + {"John", "Smith"}, + {"John", "Smith"}, + {"John", "Smith"}, + {"John", "Smith"}, + {"John", "Smith"}, + } + + for i := range testInputs { + firstName, lastName := utils.GetFirstAndLastName(testInputs[i]) + assert.Equal(t, expectedResults[i][0], firstName) + assert.Equal(t, expectedResults[i][1], lastName) + } +} diff --git a/cla-backend-go/tests/utils_test.go b/cla-backend-go/tests/utils_test.go index 8ed9fb5f0..235a82bd5 100644 --- a/cla-backend-go/tests/utils_test.go +++ b/cla-backend-go/tests/utils_test.go @@ -9,8 +9,6 @@ import ( "testing" "time" - "github.com/gofrs/uuid" - "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/stretchr/testify/assert" @@ -89,6 +87,8 @@ func TestParseDateTimeMS(t *testing.T) { "2020-03-27T15:04:05.000000+0000", "2016-12-02T05:14:05.000000+0800", "2006-08-31T10:24:05.000000-1000", + "2019-04-15T20:30:12.13589", + "2019-04-15T20:30:19.321645", } for _, dateTimeStr := range validInput { @@ -212,126 +212,6 @@ func TestTrimRemoveTrailingSpace(t *testing.T) { } -// TestValidEmail tests the email validator -func TestValidEmail(t *testing.T) { - validEmails := []string{ - "user@linuxfoundation.org", - "user+test@linuxfoundation.org", - } - inValidEmails := []string{ - "user@linuxfoundation_org", - "user/linuxfoundation.org", - "userlinuxfoundation.org", - } - - for _, email := range validEmails { - assert.True(t, utils.ValidEmail(email), fmt.Sprintf("valid email %s", email)) - } - - for _, email := range inValidEmails { - assert.False(t, utils.ValidEmail(email), fmt.Sprintf("invalid email %s", email)) - } -} - -// TestValidDomain tests the domain validator -func TestValidDomain(t *testing.T) { - validDomains := []string{ - "linuxfoundation.org", - "wikipedia.org", - "google.com", - "slack.com", - "slack-domain-with-dash.com", - } - inValidDomains := []string{ - "linuxfoundation_org", - "/linuxfoundation.org", - "linuxfoundation+fun.org", - "user_linuxfoundation.org", - } - - for _, domain := range validDomains { - msg, valid := utils.ValidDomain(domain) - assert.True(t, valid, fmt.Sprintf("valid domain %s %s", domain, msg)) - } - - for _, domain := range inValidDomains { - msg, valid := utils.ValidDomain(domain) - assert.False(t, valid, fmt.Sprintf("invalid domain %s %s", domain, msg)) - } -} - -// TestGitHubUsername tests the GitHub username validator -func TestGitHubUsername(t *testing.T) { - validGitHubUsername := []string{ - "linuxfoundation", - "user123", - "user_123", - "user_name_with_underscores", - } - inValidGitHubUsername := []string{ - "li", // too short - "/linuxfoundation", - "linuxfoundation+fun", - "user&linuxfoundation", - "user{linuxfoundation", - "user}linuxfoundation", - "user*linuxfoundation", - "user@linuxfoundation", - "user!linuxfoundation", - "user^linuxfoundation", - "++userlinuxfoundation", - "\\userlinuxfoundation", - } - - for _, username := range validGitHubUsername { - msg, valid := utils.ValidGitHubUsername(username) - assert.True(t, valid, fmt.Sprintf("valid GitHub Username %s %s", username, msg)) - } - - for _, username := range inValidGitHubUsername { - msg, valid := utils.ValidGitHubUsername(username) - assert.False(t, valid, fmt.Sprintf("invalid GitHub Username %s %s", username, msg)) - } -} - -// TestGitHubOrg tests the GitHub username validator -func TestGitHubOrg(t *testing.T) { - validGitHubOrg := []string{ - "linuxfoundation", - "linuxfoundation.org", - "user123", - "user-123", - "user-123.org", - "user-123.com", - "user_123", - "user_name_with_underscores", - } - inValidGitHubOrg := []string{ - "li", // too short - "/linuxfoundation", - "linuxfoundation+fun", - "user&linuxfoundation", - "user{linuxfoundation", - "user}linuxfoundation", - "user*linuxfoundation", - "user@linuxfoundation", - "user!linuxfoundation", - "user^linuxfoundation", - "++userlinuxfoundation", - "\\userlinuxfoundation", - } - - for _, org := range validGitHubOrg { - msg, valid := utils.ValidGitHubOrg(org) - assert.True(t, valid, fmt.Sprintf("valid GitHub Organization %s %s", org, msg)) - } - - for _, org := range inValidGitHubOrg { - msg, valid := utils.ValidGitHubOrg(org) - assert.False(t, valid, fmt.Sprintf("invalid GitHub Organization %s %s", org, msg)) - } -} - // TestGetPathFromURL tests for getting the path for a URL func TestGetPathFromURL(t *testing.T) { input := "https://cla-signature-files-dev.s3.amazonaws.com/contract-group/66b97366-a298-4625-965e-0c292c39f9a2/template/ccla-2020-09-25T22-37-51Z.pdf" @@ -340,21 +220,3 @@ func TestGetPathFromURL(t *testing.T) { assert.Nil(t, err, "GetPathFromURL error is nil") assert.Equal(t, expected, result) } - -func TestIsUUIDv4True(t *testing.T) { - v4, err := uuid.NewV4() - assert.Nil(t, err, "NewV4 UUID is nil") - assert.True(t, utils.IsUUIDv4(v4.String()), fmt.Sprintf("%s is a v4 UUID", v4.String())) -} - -func TestIsUUIDv4LikeV2(t *testing.T) { - var b byte = 'b' - v2, err := uuid.NewV2(b) - assert.Nil(t, err, "NewV4 UUID is nil") - assert.False(t, utils.IsUUIDv4(v2.String()), fmt.Sprintf("%s is not a v4 UUID", v2.String())) -} - -func TestIsUUIDv4LikeSFID(t *testing.T) { - sfid := "0014100000TdznWAAR" - assert.False(t, utils.IsUUIDv4(sfid), fmt.Sprintf("%s is not v4 UUID", sfid)) -} diff --git a/cla-backend-go/tests/utils_validators_test.go b/cla-backend-go/tests/utils_validators_test.go new file mode 100644 index 000000000..fab0ff3e8 --- /dev/null +++ b/cla-backend-go/tests/utils_validators_test.go @@ -0,0 +1,272 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "fmt" + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" +) + +// TestValidEmail tests the email validator +func TestValidEmail(t *testing.T) { + validEmails := []string{ + "user@linuxfoundation.org", + "user+test@linuxfoundation.org", + } + inValidEmails := []string{ + "user@linuxfoundation_org", + "user/linuxfoundation.org", + "userlinuxfoundation.org", + } + + for _, email := range validEmails { + assert.True(t, utils.ValidEmail(email), fmt.Sprintf("valid email %s", email)) + } + + for _, email := range inValidEmails { + assert.False(t, utils.ValidEmail(email), fmt.Sprintf("invalid email %s", email)) + } +} + +// TestValidDomain tests the domain validator +func TestValidDomain(t *testing.T) { + validDomains := []string{ + "linuxfoundation.org", + "wikipedia.org", + "google.com", + "slack.com", + "slack-domain-with-dash.com", + } + + validWildcardDomains := []string{ + "linuxfoundation.org", + "wikipedia.org", + "google.com", + "slack.com", + "slack-domain-with-dash.com", + "*.google.com", + "*.us.google.com", + } + + inValidDomains := []string{ + "*.google.com", // test case with allowWildcards = false + "linuxfoundation_org", + "*.linuxfoundation_org", // test case with allowWildcards = false + "/linuxfoundation.org", + "linuxfoundation+fun.org", + "user_linuxfoundation.org", + } + + inWildcardValidDomains := []string{ + "linuxfoundation_org", + "/linuxfoundation.org", + "linuxfoundation+fun.org", + "*.linuxfoundation+fun.org", + "user_linuxfoundation.org", + "*.user_linuxfoundation.org", + } + + for _, domain := range validDomains { + msg, valid := utils.ValidDomain(domain, false) + assert.True(t, valid, fmt.Sprintf("valid domain %s %s", domain, msg)) + } + + for _, domain := range validWildcardDomains { + msg, valid := utils.ValidDomain(domain, true) + assert.True(t, valid, fmt.Sprintf("valid domain %s %s", domain, msg)) + } + + for _, domain := range inValidDomains { + msg, valid := utils.ValidDomain(domain, false) + assert.False(t, valid, fmt.Sprintf("invalid domain %s %s", domain, msg)) + } + + for _, domain := range inWildcardValidDomains { + msg, valid := utils.ValidDomain(domain, true) + assert.False(t, valid, fmt.Sprintf("invalid domain %s %s", domain, msg)) + } +} + +// TestGitHubUsername tests the GitHub username validator +func TestGitHubUsername(t *testing.T) { + validGitHubUsername := []string{ + "linuxfoundation", + "user123", + "user_123", + "user_name_with_underscores", + } + inValidGitHubUsername := []string{ + "li", // too short + "/linuxfoundation", + "linuxfoundation+fun", + "user&linuxfoundation", + "user{linuxfoundation", + "user}linuxfoundation", + "user*linuxfoundation", + "user@linuxfoundation", + "user!linuxfoundation", + "user^linuxfoundation", + "++userlinuxfoundation", + "\\userlinuxfoundation", + } + + for _, username := range validGitHubUsername { + msg, valid := utils.ValidGitHubUsername(username) + assert.True(t, valid, fmt.Sprintf("valid GitHub Username %s %s", username, msg)) + } + + for _, username := range inValidGitHubUsername { + msg, valid := utils.ValidGitHubUsername(username) + assert.False(t, valid, fmt.Sprintf("invalid GitHub Username %s %s", username, msg)) + } +} + +// TestGitHubOrg tests the GitHub username validator +func TestGitHubOrg(t *testing.T) { + validGitHubOrg := []string{ + "linuxfoundation", + "linuxfoundation.org", + "user123", + "user-123", + "user-123.org", + "user-123.com", + "user_123", + "user_name_with_underscores", + } + inValidGitHubOrg := []string{ + "li", // too short + "/linuxfoundation", + "linuxfoundation+fun", + "user&linuxfoundation", + "user{linuxfoundation", + "user}linuxfoundation", + "user*linuxfoundation", + "user@linuxfoundation", + "user!linuxfoundation", + "user^linuxfoundation", + "++userlinuxfoundation", + "\\userlinuxfoundation", + } + + for _, org := range validGitHubOrg { + msg, valid := utils.ValidGitHubOrg(org) + assert.True(t, valid, fmt.Sprintf("valid GitHub Organization %s %s", org, msg)) + } + + for _, org := range inValidGitHubOrg { + msg, valid := utils.ValidGitHubOrg(org) + assert.False(t, valid, fmt.Sprintf("invalid GitHub Organization %s %s", org, msg)) + } +} + +// TestGitlabUsername tests the Gitlab username validator +func TestGitlabUsername(t *testing.T) { + validGitlabUsername := []string{ + "linuxfoundationuser", + "user1234", + "user_1234", + "user_name_with_underscores_gitlab", + } + inValidGitlabUsername := []string{ + "ii", // too short + "/linuxfoundationuser", + "linuxfoundationuser+fun", + "user&linuxfoundationuser", + "user{linuxfoundationuser", + "user}linuxfoundationuser", + "user*linuxfoundationuser", + "user@linuxfoundationuser", + "user!linuxfoundationuser", + "user^linuxfoundationuser", + "++userlinuxfoundationuser", + "\\userlinuxfoundationuser", + } + + for _, username := range validGitlabUsername { + msg, valid := utils.ValidGitlabUsername(username) + assert.True(t, valid, fmt.Sprintf("valid Gitlab Username %s %s", username, msg)) + } + + for _, username := range inValidGitlabUsername { + msg, valid := utils.ValidGitlabUsername(username) + assert.False(t, valid, fmt.Sprintf("invalid Gitlab Username %s %s", username, msg)) + } +} + +// TestGitlabOrg tests the GitHub username validator +func TestGitlabOrg(t *testing.T) { + validGitlabOrg := []string{ + "linuxfoundationgrp", + "linuxfoundationgrp.org", + "user1234", + "user-1234", + "user-1234.org", + "user-1234.com", + "user_1234", + "user_name_with_underscores_gitlab", + } + inValidGitlabOrg := []string{ + "hi", // too short + "/linuxfoundationgrp", + "linuxfoundationgrp+fun", + "user&linuxfoundationgrp", + "user{linuxfoundationgrp", + "user}linuxfoundationgrp", + "user*linuxfoundationgrp", + "user@linuxfoundationgrp", + "user!linuxfoundationgrp", + "user^linuxfoundationgrp", + "++userlinuxfoundationgrp", + "\\userlinuxfoundationgrp", + } + + for _, org := range validGitlabOrg { + msg, valid := utils.ValidGitHubOrg(org) + assert.True(t, valid, fmt.Sprintf("valid GitHub Organization %s %s", org, msg)) + } + + for _, org := range inValidGitlabOrg { + msg, valid := utils.ValidGitHubOrg(org) + assert.False(t, valid, fmt.Sprintf("invalid GitHub Organization %s %s", org, msg)) + } +} +func TestIsUUIDv4True(t *testing.T) { + v4, err := uuid.NewV4() + assert.Nil(t, err, "NewV4 UUID is nil") + assert.True(t, utils.IsUUIDv4(v4.String()), fmt.Sprintf("%s is a v4 UUID", v4.String())) +} + +func TestIsUUIDv4LikeSFID(t *testing.T) { + sfid := "0014100000TdznWAAR" + assert.False(t, utils.IsUUIDv4(sfid), fmt.Sprintf("%s is not v4 UUID", sfid)) +} + +func TestIsSalesForceID(t *testing.T) { + trueTestData := []string{ + "00117000015vpjX", + "00117000015vpjXAAQ", + } + falseTestData := []string{ + "", + "00117", + "-00117", + "00117000015vpj-", + "0011700001-vpjXAAQ", + "0011700001?vpjXAAQ", + "0011700001&vpjXAAQ", + "0011700001_vpjXAAQ", + } + + for i := range trueTestData { + assert.True(t, utils.IsSalesForceID(trueTestData[i])) + } + for i := range falseTestData { + assert.False(t, utils.IsSalesForceID(falseTestData[i])) + } +} diff --git a/cla-backend-go/tests/v2_cla_manager_templates_test.go b/cla-backend-go/tests/v2_cla_manager_templates_test.go new file mode 100644 index 000000000..feb99259f --- /dev/null +++ b/cla-backend-go/tests/v2_cla_manager_templates_test.go @@ -0,0 +1,256 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/emails" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/stretchr/testify/assert" +) + +func TestV2ContributorApprovalRequestTemplate(t *testing.T) { + params := emails.V2ContributorApprovalRequestTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + Projects: []emails.CLAProjectParams{ + {ExternalProjectName: "Project Spaced 1", ProjectSFID: "ProjectSFID2", FoundationSFID: "FoundationSFID2", CorporateConsole: "http://CorporateConsole.com"}, + }, + CorporateConsole: "http://CorporateConsoleV2URL.com", + }, + UserDetails: "UserDetailsValue", + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2ContributorApprovalRequestTemplateName, emails.V2ContributorApprovalRequestTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the organization JohnsCompany") + assert.Contains(t, result, "The following contributor would like to submit a contribution to the projects(s): Project Spaced 1") + assert.Contains(t, result, "UserDetailsValue") + assert.Contains(t, result, "target=\"_blank\">Project Spaced 1
    ") + + assert.Contains(t, result, "CLA Managers can visit the EasyCLA corporate console page for Project Spaced 1") + assert.Contains(t, result, "and add the contributor to one of the approval lists.") + + params.SigningEntityName = "SigningEntityNameValue" + + result, err = emails.RenderTemplate(utils.V1, emails.V2ContributorApprovalRequestTemplateName, emails.V2ContributorApprovalRequestTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the organization JohnsCompany") + assert.Contains(t, result, "UserDetailsValue") +} + +func TestV2OrgAdminTemplate(t *testing.T) { + params := emails.V2OrgAdminTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + CompanyName: "JohnsCompany", + RecipientName: "JohnsClaManager", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + Projects: []emails.CLAProjectParams{{ + ExternalProjectName: "JohnsProject", + ProjectSFID: "ProjectSFIDValue", + FoundationSFID: "FoundationSFIDValue", + CorporateConsole: "http://CorporateConsole.com", + }}, + CLAGroupName: "JohnsCLAGroupName", + CorporateConsole: "http://CorporateConsole.com", + }, + SenderName: "SenderNameValue", + SenderEmail: "SenderEmailValue", + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2OrgAdminTemplateName, emails.V2OrgAdminTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "signing process for the organization JohnsCompany") + assert.Contains(t, result, "SenderNameValue SenderEmailValue has identified you") + assert.Contains(t, result, "Corporate CLA in support of the following project(s):") + assert.Contains(t, result, "
  • JohnsProject
  • ") + assert.Contains(t, result, "can login to the EasyCLA portal") + assert.Contains(t, result, `sign the CLA for this project JohnsProject`) +} + +func TestV2ContributorToOrgAdminTemplate(t *testing.T) { + params := emails.V2ContributorToOrgAdminTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + Projects: []emails.CLAProjectParams{ + {ExternalProjectName: "Project1", ProjectSFID: "ProjectSFID1", FoundationSFID: "FoundationSFID1", CorporateConsole: "http://CorporateConsole.com"}, + {ExternalProjectName: "Project2", ProjectSFID: "ProjectSFID2", FoundationSFID: "FoundationSFID2", CorporateConsole: "http://CorporateConsole.com"}, + }, + CLAGroupName: "JohnsCLAGroupName", + CorporateConsole: "http://CorporateConsole.com", + }, + + UserDetails: "UserDetailsValue", + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2ContributorToOrgAdminTemplateName, emails.V2ContributorToOrgAdminTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "would like to submit a contribution to Project1,Project2") + assert.Contains(t, result, "your organization must sign a CLA.") + 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, "
  • JohnsProject
  • ") + assert.Contains(t, result, "can login and sign the CLA for this project JohnsProject") +} + +func TestV2ToCLAManagerDesigneeTemplate(t *testing.T) { + params := emails.V2ToCLAManagerDesigneeTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "JohnsClaManager", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + Projects: []emails.CLAProjectParams{ + {ExternalProjectName: "Project1", ProjectSFID: "ProjectSFID1", FoundationSFID: "FoundationSFID1", CorporateConsole: "http://CorporateConsole.com"}, + {ExternalProjectName: "Project2", ProjectSFID: "ProjectSFID2", FoundationSFID: "FoundationSFID2", CorporateConsole: "http://CorporateConsole.com"}, + }, + CorporateConsole: "http://CorporateConsole.com", + }, + Contributor: emails.Contributor{ + Email: "ContributorEmailValue", + Username: "ContributorNameValue", + EmailLabel: utils.EmailLabel, + UsernameLabel: utils.UserLabel, + }, + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2ToCLAManagerDesigneeTemplateName, emails.V2ToCLAManagerDesigneeTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project(s): Project1, Project2") + assert.Contains(t, result, "from Username: ContributorNameValue (Email Address: ContributorEmailValue)") + assert.Contains(t, result, `CLA for any of the project(s): Project1,Project2`) + + params.Projects = []emails.CLAProjectParams{ + {ExternalProjectName: "Project1", ProjectSFID: "ProjectSFID1", FoundationSFID: "FoundationSFID1", CorporateConsole: "http://CorporateConsole.com"}, + } + params.Contributor.EmailLabel = utils.GitHubEmailLabel + params.Contributor.UsernameLabel = utils.GitHubUserLabel + + result, err = emails.RenderTemplate(utils.V1, emails.V2ToCLAManagerDesigneeTemplateName, emails.V2ToCLAManagerDesigneeTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the project(s): Project1") + assert.Contains(t, result, "from GitHub Username: ContributorNameValue (GitHub Email Address: ContributorEmailValue)") + assert.Contains(t, result, `CLA for any of the project(s): Project1`) + +} + +func TestV2DesigneeToUserWithNoLFIDTemplate(t *testing.T) { + params := emails.V2ToCLAManagerDesigneeTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "JohnsClaManager", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + Projects: []emails.CLAProjectParams{ + {ExternalProjectName: "Project1", ProjectSFID: "ProjectSFID1", FoundationSFID: "FoundationSFID1", CorporateConsole: "https://corporate.dev.lfcla.com"}, + {ExternalProjectName: "Project2", ProjectSFID: "ProjectSFID2", FoundationSFID: "FoundationSFID2", CorporateConsole: "https://corporate.dev.lfcla.com"}, + }, + CorporateConsole: "https://corporate.dev.lfcla.com", + }, + + Contributor: emails.Contributor{ + Email: "ContributorEmail", + Username: "ContributorUsername", + EmailLabel: utils.EmailLabel, + UsernameLabel: utils.UserLabel, + }, + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2DesigneeToUserWithNoLFIDTemplateName, emails.V2DesigneeToUserWithNoLFIDTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager,") + assert.Contains(t, result, "We received a request from Username: ContributorUsername (Email Address: ContributorEmail)") + assert.Contains(t, result, "After login, you will be redirected to the portal https://corporate.dev.lfcla.com ") + assert.Contains(t, result, `where you can either sign the CLA for any of the project(s): Project1`) + assert.Contains(t, result, "or send it to an authorized signatory for your company.") + + params.Contributor.EmailLabel = utils.GitHubEmailLabel + params.Contributor.UsernameLabel = utils.GitHubUserLabel + + result, err = emails.RenderTemplate(utils.V1, emails.V2DesigneeToUserWithNoLFIDTemplateName, emails.V2DesigneeToUserWithNoLFIDTemplate, + params) + + assert.NoError(t, err) + assert.Contains(t, result, "We received a request from GitHub Username: ContributorUsername (GitHub Email Address: ContributorEmail)") +} + +func TestV2CLAManagerToUserWithNoLFIDTemplate(t *testing.T) { + params := emails.V2CLAManagerToUserWithNoLFIDTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: "JohnsClaManager", + CompanyName: "JohnsCompany", + }, + CLAGroupTemplateParams: emails.CLAGroupTemplateParams{ + Projects: []emails.CLAProjectParams{{ExternalProjectName: "JohnsProjectExternal", + CorporateConsole: "http://CorporateConsole.com", + SignedAtFoundationLevel: false, + ProjectSFID: "ProjectSFID", + FoundationSFID: "FoundationSFID", + }}, + CLAGroupName: "JohnsCLAGroupName", + }, + RequesterUserName: "RequesterUserNameValue", + RequesterEmail: "RequesterEmailValue", + } + + result, err := emails.RenderTemplate(utils.V1, emails.V2CLAManagerToUserWithNoLFIDTemplateName, emails.V2CLAManagerToUserWithNoLFIDTemplate, + params) + assert.NoError(t, err) + assert.Contains(t, result, "Hello JohnsClaManager") + assert.Contains(t, result, "regarding the CLA setup and signing process for the organization JohnsCompany") + assert.Contains(t, result, "The user RequesterUserNameValue (RequesterEmailValue) has identified you as a potential candidate to setup the Corporate CLA for the organization JohnsCompany and the project JohnsProjectExternal") + assert.Contains(t, result, "After login, you will be redirected to the portal http://CorporateConsole.com") + assert.Contains(t, result, "After adding the contributor, please notify them") + assert.Contains(t, result, `where you can either sign the CLA for the project: JohnsProjectExternal`) +} diff --git a/cla-backend-go/tools/regenmocks.sh b/cla-backend-go/tools/regenmocks.sh new file mode 100755 index 000000000..bfb9e2b25 --- /dev/null +++ b/cla-backend-go/tools/regenmocks.sh @@ -0,0 +1,14 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT +#!/bin/bash + +mkdir -p repositories/mock +mkdir -p events/mock +mkdir -p github_organizations/mock + +# interfaces +mockgen -copyright_file=copyright-header.txt -source=repositories/service.go -destination=repositories/mock/mock_service.go -package=mock +mockgen -copyright_file=copyright-header.txt -source=repositories/repository.go -destination=repositories/mock/mock_repository.go -package=mock +mockgen -copyright_file=copyright-header.txt -source=github_organizations/repository.go -destination=github_organizations/mock/mock_repository.go -package=mock RepositoryInterface +mockgen -copyright_file=copyright-header.txt -source=events/service.go -destination=events/mock/mock_service.go -package=mock Service +mockgen -copyright_file=copyright-header.txt -source=events/repository.go -destination=events/mock/mock_repository.go -package=mock RepositoryInterface \ No newline at end of file diff --git a/cla-backend-go/user/repository.go b/cla-backend-go/user/repository.go index 976c2e463..c531c0506 100644 --- a/cla-backend-go/user/repository.go +++ b/cla-backend-go/user/repository.go @@ -11,26 +11,27 @@ import ( "github.com/jmoiron/sqlx" ) -// Repository interface methods -type Repository interface { +// RepositoryInterface interface methods +type RepositoryInterface interface { GetUserAndProfilesByLFID(lfidUsername string) (CLAUser, error) GetUserProjectIDs(userID string) ([]string, error) GetClaManagerCorporateClaIDs(userID string) ([]string, error) } -type repository struct { +// Repository object/struct +type Repository struct { db *sqlx.DB } // NewRepository creates a new user repository -func NewRepository(db *sqlx.DB) repository { - return repository{ +func NewRepository(db *sqlx.DB) Repository { + return Repository{ db: db, } } // GetUserAndProfilesByLFID get user profile by LFID -func (repo repository) GetUserAndProfilesByLFID(lfidUsername string) (CLAUser, error) { +func (repo Repository) GetUserAndProfilesByLFID(lfidUsername string) (CLAUser, error) { log.Debugf("lfidUsername: %s", lfidUsername) sql := ` SELECT @@ -74,13 +75,13 @@ func (repo repository) GetUserAndProfilesByLFID(lfidUsername string) (CLAUser, e } // GetUserByGithubID returns the user details based on the github ID -func (repo repository) GetUserByGithubID(githubID string) (CLAUser, error) { +func (repo Repository) GetUserByGithubID(githubID string) (CLAUser, error) { // TODO: Implement when adding authentication to the Corporate Console return CLAUser{}, nil } // GetUserProjectIDs get the user project ID's based on the specified user ID -func (repo repository) GetUserProjectIDs(userID string) ([]string, error) { +func (repo Repository) GetUserProjectIDs(userID string) ([]string, error) { getUserProjectIDsSQL := ` SELECT project_sfdc_id @@ -113,7 +114,7 @@ func (repo repository) GetUserProjectIDs(userID string) ([]string, error) { } // GetClaManagerCorporateClaIDs returns a list of CLA manager corporate CLAs associated with the specified user -func (repo repository) GetClaManagerCorporateClaIDs(userID string) ([]string, error) { +func (repo Repository) GetClaManagerCorporateClaIDs(userID string) ([]string, error) { getClaManagerCorporateClaIDsSQL := ` SELECT corporate_cla_group_id @@ -146,6 +147,6 @@ func (repo repository) GetClaManagerCorporateClaIDs(userID string) ([]string, er } // GetUserCompanyIDs returns a list of company IDs based on the user -func (repo repository) GetUserCompanyIDs(userID string) ([]string, error) { +func (repo Repository) GetUserCompanyIDs(userID string) ([]string, error) { return []string{}, nil } diff --git a/cla-backend-go/user/service.go b/cla-backend-go/user/service.go index 2ed8164d8..24daf8b0a 100644 --- a/cla-backend-go/user/service.go +++ b/cla-backend-go/user/service.go @@ -11,11 +11,11 @@ type Service interface { // nolint } type service struct { - repo Repository + repo RepositoryInterface } // NewService creates a new user service -func NewService(repo Repository) Service { +func NewService(repo RepositoryInterface) Service { return service{ repo: repo, } diff --git a/cla-backend-go/userSubscribeLambda/main.go b/cla-backend-go/userSubscribeLambda/main.go deleted file mode 100644 index d115f380a..000000000 --- a/cla-backend-go/userSubscribeLambda/main.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -package main - -import ( - "context" - "fmt" - "os" - "runtime" - - "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/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/userSubscribeLambda/cmd" - "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() { - var awsSession = session.Must(session.NewSession(&aws.Config{})) - stage := os.Getenv("STAGE") - if stage == "" { - log.Fatal("stage not set") - } - log.Infof("STAGE set to %s\n", stage) - configFile, err := config.LoadConfig("", awsSession, stage) - if err != nil { - log.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 { - if len(snsEvent.Records) == 0 { - log.Warn("SNS event contained 0 records - ignoring message.") - return nil - } - - for _, message := range snsEvent.Records { - log.Infof("Processing message id: '%s' for event source '%s'\n", message.SNS.MessageID, message.EventSource) - - log.Debugf("Unmarshalling message body: '%s'", message.SNS.Message) - - // log.Debugf("Unmarshalling message body: '%s'", message.SNS.Message) - var model event.Event - err := model.UnmarshalBinary([]byte(message.SNS.Message)) - if err != nil { - log.Warnf("Error: %v, JSON unmarshal failed - unable to process message: %s", err, message.SNS.MessageID) - return err - } - - switch model.Type { - case "UserUpdatedProfile": - Write(model) - default: - log.Warnf("unrecognized message type: %s - unable to process message ", model.Type) - } - - } - return nil -} - -// Write saves the user data model to persistent storage -func Write(user event.Event) { - - uc := &usersModels.UserUpdated{} - err := mapstructure.Decode(user.Data, uc) - if err != nil { - 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.Warnf("Error - unable to locate user by LfUsername: %s, error: %+v", *uc.Username, userErr) - log.Error("", userErr) - } - - if userDetails == nil { - for _, email := range uc.Emails { - userDetails, userErr = usersRepo.GetUserByEmail(*email.EmailAddress) - if userErr != nil { - log.Warnf("Error - unable to locate user by LfUsername: %s, error: %+v", *uc.Username, userErr) - } - } - } - - if userDetails == nil { - userDetails, userErr = usersRepo.GetUserByExternalID(uc.UserID) - if userErr != nil { - log.Warnf("Error - unable to locate user by UserExternalID: %s, error: %+v", uc.UserID, userErr) - } - } - - if userDetails == nil { - log.Debugf("User model is nil so skipping user %s", *uc.Username) - return - } - - userServiceClient := user_service.GetClient() - - sfdcUserObject, err := userServiceClient.GetUser(uc.UserID) - if err != nil { - log.Warnf("Error - unable to locate user by SFID: %s, error: %+v", uc.UserID, userErr) - log.Error("", userErr) - return - } - - log.Debugf("Salesforce user-service object : %+v", sfdcUserObject) - - if sfdcUserObject == nil { - log.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{ - 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), - Emails: emails, - } - - log.Debugf("Updating user in Dynamo DB : %+v", updateUserModel) - - _, updateErr := usersRepo.Save(updateUserModel) - if updateErr != nil { - log.Warnf("Error - unable to update user by LfUsername: %s, error: %+v", *uc.Username, updateErr) - return - } -} - -func main() { - var err error - - // Show the version and build info - log.Infof("Name : userSubscribe handler") - log.Infof("Version : %s", version) - log.Infof("Git commit hash : %s", commit) - log.Infof("Build date : %s", buildDate) - log.Infof("Golang OS : %s", runtime.GOOS) - log.Infof("Golang Arch : %s", runtime.GOARCH) - - err = cmd.Start(Handler) - if err != nil { - log.Fatal(err) - } -} diff --git a/cla-backend-go/users/handlers.go b/cla-backend-go/users/handlers.go index 9205285b7..3350a3391 100644 --- a/cla-backend-go/users/handlers.go +++ b/cla-backend-go/users/handlers.go @@ -6,12 +6,14 @@ package users import ( "fmt" + "github.com/go-openapi/strfmt" + "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/users" + "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/users" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/go-openapi/runtime/middleware" @@ -43,7 +45,7 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser } newUser := &models.User{ - LfEmail: claUser.LFEmail, + LfEmail: strfmt.Email(claUser.LFEmail), LfUsername: claUser.LFUsername, Username: claUser.Name, } @@ -74,7 +76,7 @@ func Configure(api *operations.ClaAPI, service Service, eventsService events.Ser } // Update supports two scenarios: // 1) user has LF login and their record has the LF login as part of their existing User record - should find and match - OK, otherwise permission denied - // 2) user has new LF login and their record does not have the LF login as part of their existing User record - need to lookup by other means, such as Github Username + // 2) user has new LF login and their record does not have the LF login as part of their existing User record - need to lookup by other means, such as GitHub Username // option 2 can happen when GH user gets a user record auto-created and later they need a login for v2 (create company, etc.) // option 2 will be called after they create their login to update their user record with the new login details // option 2 we will search by github username to find the old record - but we can't compare LF login with the existing record because it won't be set yet diff --git a/cla-backend-go/users/mocks/mock_repo.go b/cla-backend-go/users/mocks/mock_repo.go new file mode 100644 index 000000000..3a603a729 --- /dev/null +++ b/cla-backend-go/users/mocks/mock_repo.go @@ -0,0 +1,276 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: users/repository.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" + gomock "github.com/golang/mock/gomock" +) + +// MockUserRepository is a mock of UserRepository interface. +type MockUserRepository struct { + ctrl *gomock.Controller + recorder *MockUserRepositoryMockRecorder +} + +// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository. +type MockUserRepositoryMockRecorder struct { + mock *MockUserRepository +} + +// NewMockUserRepository creates a new mock instance. +func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository { + mock := &MockUserRepository{ctrl: ctrl} + mock.recorder = &MockUserRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { + return m.recorder +} + +// CreateUser mocks base method. +func (m *MockUserRepository) CreateUser(user *models.User) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", user) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockUserRepositoryMockRecorder) CreateUser(user interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUserRepository)(nil).CreateUser), user) +} + +// Delete mocks base method. +func (m *MockUserRepository) Delete(userID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockUserRepositoryMockRecorder) Delete(userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUserRepository)(nil).Delete), userID) +} + +// GetUser mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) GetUser(userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockUserRepository)(nil).GetUser), userID) +} + +// GetUserByEmail mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) GetUserByEmail(userEmail interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockUserRepository)(nil).GetUserByEmail), userEmail) +} + +// GetUserByExternalID mocks base method. +func (m *MockUserRepository) GetUserByExternalID(userExternalID string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByExternalID", userExternalID) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByExternalID indicates an expected call of GetUserByExternalID. +func (mr *MockUserRepositoryMockRecorder) GetUserByExternalID(userExternalID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByExternalID", reflect.TypeOf((*MockUserRepository)(nil).GetUserByExternalID), userExternalID) +} + +// GetUserByGitHubID mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) GetUserByGitHubID(gitHubID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitHubID", reflect.TypeOf((*MockUserRepository)(nil).GetUserByGitHubID), gitHubID) +} + +// GetUserByGitHubUsername mocks base method. +func (m *MockUserRepository) GetUserByGitHubUsername(gitHubUsername string) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByGitHubUsername", gitHubUsername) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByGitHubUsername indicates an expected call of GetUserByGitHubUsername. +func (mr *MockUserRepositoryMockRecorder) GetUserByGitHubUsername(gitHubUsername interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitHubUsername", reflect.TypeOf((*MockUserRepository)(nil).GetUserByGitHubUsername), gitHubUsername) +} + +// GetUserByGitLabUsername mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) GetUserByGitLabUsername(gitlabUsername interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitLabUsername", reflect.TypeOf((*MockUserRepository)(nil).GetUserByGitLabUsername), gitlabUsername) +} + +// GetUserByGitlabID mocks base method. +func (m *MockUserRepository) GetUserByGitlabID(gitlabID int) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByGitlabID", gitlabID) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByGitlabID indicates an expected call of GetUserByGitlabID. +func (mr *MockUserRepositoryMockRecorder) GetUserByGitlabID(gitlabID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByGitlabID", reflect.TypeOf((*MockUserRepository)(nil).GetUserByGitlabID), gitlabID) +} + +// GetUserByLFUserName mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) GetUserByLFUserName(lfUserName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByLFUserName", reflect.TypeOf((*MockUserRepository)(nil).GetUserByLFUserName), lfUserName) +} + +// GetUserByUserName mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) GetUserByUserName(userName, fullMatch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUserName", reflect.TypeOf((*MockUserRepository)(nil).GetUserByUserName), userName, fullMatch) +} + +// GetUsersByEmail mocks base method. +func (m *MockUserRepository) GetUsersByEmail(userEmail string) ([]*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersByEmail", userEmail) + ret0, _ := ret[0].([]*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUsersByEmail indicates an expected call of GetUsersByEmail. +func (mr *MockUserRepositoryMockRecorder) GetUsersByEmail(userEmail interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByEmail", reflect.TypeOf((*MockUserRepository)(nil).GetUsersByEmail), userEmail) +} + +// Save mocks base method. +func (m *MockUserRepository) Save(user *models.UserUpdate) (*models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Save", user) + ret0, _ := ret[0].(*models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Save indicates an expected call of Save. +func (mr *MockUserRepositoryMockRecorder) Save(user interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockUserRepository)(nil).Save), user) +} + +// SearchUsers mocks base method. +func (m *MockUserRepository) SearchUsers(searchField, searchTerm string, fullMatch bool) (*models.Users, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchUsers", searchField, searchTerm, fullMatch) + ret0, _ := ret[0].(*models.Users) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchUsers indicates an expected call of SearchUsers. +func (mr *MockUserRepositoryMockRecorder) SearchUsers(searchField, searchTerm, fullMatch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsers", reflect.TypeOf((*MockUserRepository)(nil).SearchUsers), searchField, searchTerm, fullMatch) +} + +// UpdateUser mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) UpdateUser(userID, updates interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockUserRepository)(nil).UpdateUser), userID, updates) +} + +// UpdateUserCompanyID mocks base method. +func (m *MockUserRepository) 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 *MockUserRepositoryMockRecorder) UpdateUserCompanyID(userID, companyID, note interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserCompanyID", reflect.TypeOf((*MockUserRepository)(nil).UpdateUserCompanyID), userID, companyID, note) +} diff --git a/cla-backend-go/users/models.go b/cla-backend-go/users/models.go index 30196061d..3a0b8f52d 100644 --- a/cla-backend-go/users/models.go +++ b/cla-backend-go/users/models.go @@ -16,7 +16,13 @@ type DBUser struct { Version string `json:"version"` UserEmails []string `json:"user_emails"` UserGithubID string `json:"user_github_id"` - UserCompanyID string `json:"user_company_id"` UserGithubUsername string `json:"user_github_username"` + UserGitlabID string `json:"user_gitlab_id"` + UserGitlabUsername string `json:"user_gitlab_username"` + UserCompanyID string `json:"user_company_id"` Note string `json:"note"` } + +type UserEmails struct { + SS []string `json:"SS"` +} diff --git a/cla-backend-go/users/repository.go b/cla-backend-go/users/repository.go index 51dc21b76..ccfa88e11 100644 --- a/cla-backend-go/users/repository.go +++ b/cla-backend-go/users/repository.go @@ -9,6 +9,10 @@ import ( "strings" "time" + "github.com/go-openapi/strfmt" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" "github.com/go-openapi/errors" @@ -19,7 +23,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb/expression" "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" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/aws/aws-sdk-go/aws" @@ -31,14 +35,20 @@ import ( type UserRepository interface { CreateUser(user *models.User) (*models.User, error) Save(user *models.UserUpdate) (*models.User, error) + UpdateUser(userID string, updates map[string]interface{}) (*models.User, error) Delete(userID string) error GetUser(userID string) (*models.User, error) GetUserByLFUserName(lfUserName string) (*models.User, error) GetUserByExternalID(userExternalID string) (*models.User, error) GetUserByUserName(userName string, fullMatch bool) (*models.User, error) GetUserByEmail(userEmail string) (*models.User, error) + GetUserByGitHubID(gitHubID string) (*models.User, error) GetUserByGitHubUsername(gitHubUsername string) (*models.User, error) + GetUserByGitlabID(gitlabID int) (*models.User, error) + GetUserByGitLabUsername(gitlabUsername string) (*models.User, error) SearchUsers(searchField string, searchTerm string, fullMatch bool) (*models.Users, error) + UpdateUserCompanyID(userID, companyID, note string) error + GetUsersByEmail(userEmail string) ([]*models.User, error) } // repository data model @@ -62,6 +72,7 @@ func (repo repository) CreateUser(user *models.User) (*models.User, error) { f := logrus.Fields{ "functionName": "users.repository.CreateUser", } + theUUID, err := uuid.NewUUID() if err != nil { return nil, err @@ -88,7 +99,7 @@ func (repo repository) CreateUser(user *models.User) (*models.User, error) { if user.GithubID != "" { attributes["user_github_id"] = &dynamodb.AttributeValue{ - S: aws.String(user.GithubID), + N: aws.String(user.GithubID), } } @@ -98,9 +109,27 @@ func (repo repository) CreateUser(user *models.User) (*models.User, error) { } } + if user.GitlabID != "" { + attributes["user_gitlab_id"] = &dynamodb.AttributeValue{ + N: aws.String(user.GitlabID), + } + } + + if user.GitlabUsername != "" { + attributes["user_gitlab_username"] = &dynamodb.AttributeValue{ + S: aws.String(user.GitlabUsername), + } + } + if user.LfEmail != "" { attributes["lf_email"] = &dynamodb.AttributeValue{ - S: aws.String(user.LfEmail), + S: aws.String(user.LfEmail.String()), + } + } + + if len(user.Emails) > 0 { + attributes["user_emails"] = &dynamodb.AttributeValue{ + SS: utils.ArrayStringPointer(user.Emails), } } @@ -115,6 +144,19 @@ func (repo repository) CreateUser(user *models.User) (*models.User, error) { S: aws.String(user.Username), } } + + if user.CompanyID != "" { + attributes["user_company_id"] = &dynamodb.AttributeValue{ + S: aws.String(user.CompanyID), + } + } + + if user.Note != "" { + attributes["note"] = &dynamodb.AttributeValue{ + S: aws.String(user.Note), + } + } + now := time.Now().UTC().Format(time.RFC3339) user.DateCreated = now @@ -172,6 +214,63 @@ func (repo repository) CreateUser(user *models.User) (*models.User, error) { return user, err } +func (repo repository) UpdateUser(userID string, updates map[string]interface{}) (*models.User, error) { + f := logrus.Fields{ + "functionName": "users.repository.UpdateUser", + "userID": userID, + } + + log.WithFields(f).Debugf("Updating user: %s with updates: %+v", userID, updates) + + if len(updates) == 0 { + return nil, errors.New(400, "no updates provided") + } + + var updateExpression strings.Builder + updateExpression.WriteString("SET ") + attributeValues := make(map[string]*dynamodb.AttributeValue) + attributeNames := make(map[string]*string) + + count := 1 + for key, value := range updates { + attrPlaceholder := fmt.Sprintf("#A%d", count) + valPlaceholder := fmt.Sprintf(":v%d", count) + + if count > 1 { + updateExpression.WriteString(", ") + } + updateExpression.WriteString(fmt.Sprintf("%s = %s", attrPlaceholder, valPlaceholder)) + attributeNames[attrPlaceholder] = aws.String(key) + + av, err := dynamodbattribute.Marshal(value) + if err != nil { + return nil, err + } + attributeValues[valPlaceholder] = av + + count++ + } + + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: attributeNames, + ExpressionAttributeValues: attributeValues, + Key: map[string]*dynamodb.AttributeValue{ + "user_id": { + S: aws.String(userID), + }, + }, + TableName: aws.String(repo.tableName), + UpdateExpression: aws.String(updateExpression.String()), + } + + _, err := repo.dynamoDBClient.UpdateItem(input) + if err != nil { + return nil, err + } + + return repo.GetUser(userID) +} + func (repo repository) getUserByUpdateModel(user *models.UserUpdate) (*models.User, error) { // Log fields f := logrus.Fields{ @@ -255,7 +354,7 @@ func (repo repository) Save(user *models.UserUpdate) (*models.User, error) { expressionAttributeValues := map[string]*dynamodb.AttributeValue{} updateExpression := "SET " - if user.LfEmail != "" && oldUserModel.LfEmail != user.LfEmail { + if user.LfEmail != "" && oldUserModel.LfEmail.String() != user.LfEmail { log.WithFields(f).Debugf("building query - adding lf_email: %s", user.LfEmail) expressionAttributeNames["#E"] = aws.String("lf_email") expressionAttributeValues[":e"] = &dynamodb.AttributeValue{S: aws.String(user.LfEmail)} @@ -425,7 +524,7 @@ func (repo repository) GetUser(userID string) (*models.User, error) { return convertDBUserModel(dbUserModels[0]), nil } -// GetuserByLFUserName returns the user record associated with the LF Username value +// GetUserByLFUserName returns the user record associated with the LF Username value func (repo repository) GetUserByLFUserName(lfUserName string) (*models.User, error) { f := logrus.Fields{ "functionName": "users.repository.GetUserByLFUserName", @@ -461,6 +560,8 @@ func (repo repository) GetUserByLFUserName(lfUserName string) (*models.User, err return nil, err } + log.WithFields(f).Debugf("result: %+v", result.Items) + // The user model var dbUserModels []DBUser @@ -545,7 +646,7 @@ func (repo repository) GetUserByUserName(userName string, fullMatch bool) (*mode var condition expression.KeyConditionBuilder if strings.Contains(userName, "github:") { - indexName = "github-user-index" + indexName = "github-id-index" // Username for GitHub comes in as github:123456, so we want to remove the initial string githubID, err := strconv.Atoi(strings.Replace(userName, "github:", "", 1)) if err != nil { @@ -632,6 +733,87 @@ func (repo repository) GetUserByUserName(userName string, fullMatch bool) (*mode return nil, nil } +func (repo repository) GetUsersByEmail(userEmail string) ([]*models.User, error) { + f := logrus.Fields{ + "functionName": "users.repository.GetUsersByEmail", + "userEmail": userEmail, + } + + // This is the filter we want to match + filter := expression.Name("user_emails").Contains(userEmail) + + // These are the columns we want returned + projection := buildUserProjection() + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithFilter(filter).WithProjection(projection).Build() + if err != nil { + log.WithFields(f).Warnf("error building expression for lf_email : %s, error: %v", userEmail, err) + return nil, err + } + + // Assemble the scan input parameters + scanInput := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.tableName), + } + + lastEvaluatedKey := "" + resultItems := []map[string]*dynamodb.AttributeValue{} + + for ok := true; ok; ok = lastEvaluatedKey != "" { + var result *dynamodb.ScanOutput + // Make the DynamoDB Query API call + log.WithFields(f).Debugf("lastEvaluatedKey: %s", lastEvaluatedKey) + if lastEvaluatedKey != "" { + scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ + "user_id": { + S: aws.String(lastEvaluatedKey), + }, + } + result, err = repo.dynamoDBClient.Scan(scanInput) + if err != nil { + log.WithFields(f).Warnf("Error retrieving user by user email: %s, error: %+v", userEmail, err) + return nil, err + } + } else { + result, err = repo.dynamoDBClient.Scan(scanInput) + if err != nil { + log.WithFields(f).Warnf("Error retrieving user by user email: %s, error: %+v", userEmail, err) + return nil, err + } + } + resultItems = append(resultItems, result.Items...) + + // If we have another page of results... + if result.LastEvaluatedKey["user_id"] != nil { + lastEvaluatedKey = *result.LastEvaluatedKey["user_id"].S + } else { + lastEvaluatedKey = "" + } + } + + // The database user model + var dbUserModels []DBUser + + err = dynamodbattribute.UnmarshalListOfMaps(resultItems, &dbUserModels) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling user record from database for user email: %s, error: %+v", userEmail, err) + return nil, err + } + + users := make([]*models.User, 0, len(dbUserModels)) + for _, dbUser := range dbUserModels { + users = append(users, convertDBUserModel(dbUser)) + log.WithFields(f).Debugf("found DB user ID: %+s and user Emails: %s", dbUser.UserID, dbUser.UserEmails) + } + + return users, nil +} + // GetUserByEmail fetches the user record by email func (repo repository) GetUserByEmail(userEmail string) (*models.User, error) { f := logrus.Fields{ @@ -678,9 +860,73 @@ func (repo repository) GetUserByEmail(userEmail string) (*models.User, error) { } if len(dbUserModels) == 0 { - return nil, errors.NotFound("user not found when searching by lf_email: %s", userEmail) + return nil, &utils.UserNotFound{ + Message: fmt.Sprintf("user not found when searching by lf email: %s", userEmail), + UserLFID: "", + UserName: "", + UserEmail: userEmail, + Err: nil, + } + } else if len(dbUserModels) > 1 { + log.WithFields(f).Warnf("retrieved %d results for the lf_email query when we should return 0 or 1", len(dbUserModels)) + } + + return convertDBUserModel(dbUserModels[0]), nil +} + +// GetUserByGitHubID fetches the user record by github ID +func (repo repository) GetUserByGitHubID(gitHubID string) (*models.User, error) { + f := logrus.Fields{ + "functionName": "users.repository.GetUserByGitHubID", + "gitHubID": gitHubID, + } + // This is the key we want to match + intGitHubID, atoiErr := strconv.Atoi(gitHubID) + if atoiErr != nil { + return nil, atoiErr + } + condition := expression.Key("user_github_id").Equal(expression.Value(intGitHubID)) + + // These are the columns we want returned + projection := buildUserProjection() + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(projection).Build() + if err != nil { + log.WithFields(f).WithError(err).Warnf("error building expression for user_github_id : %s, error: %v", gitHubID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.tableName), + IndexName: aws.String("github-id-index"), + } + + // Make the DynamoDB Query API call + result, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving user by user_github_id: %s, error: %+v", gitHubID, err) + return nil, err + } + + // The user model + var dbUserModels []DBUser + + err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &dbUserModels) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error unmarshalling user record from database for user_github_id: %s, error: %+v", gitHubID, err) + return nil, err + } + + if len(dbUserModels) == 0 { + return nil, errors.NotFound("user not found when searching by user_github_id: %s", gitHubID) } else if len(dbUserModels) > 1 { - log.WithFields(f).WithError(err).Warnf("retrieved %d results for the lf_email query when we should return 0 or 1", len(dbUserModels)) + log.WithFields(f).WithError(err).Warnf("retrieved %d results for the user_github_id query when we should return 0 or 1", len(dbUserModels)) } return convertDBUserModel(dbUserModels[0]), nil @@ -740,6 +986,114 @@ func (repo repository) GetUserByGitHubUsername(gitHubUsername string) (*models.U return convertDBUserModel(dbUserModels[0]), nil } +// GetUserByGitlabID fetches the user record by gitlab ID +func (repo repository) GetUserByGitlabID(gitlabID int) (*models.User, error) { + f := logrus.Fields{ + "functionName": "users.repository.GetUserByGitlabID", + "gitlabID": gitlabID, + } + // This is the key we want to match + condition := expression.Key("user_gitlab_id").Equal(expression.Value(gitlabID)) + + // These are the columns we want returned + projection := buildUserProjection() + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(projection).Build() + if err != nil { + log.WithFields(f).WithError(err).Warnf("error building expression for user_gitlab_id : %d, error: %v", gitlabID, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.tableName), + IndexName: aws.String("gitlab-id-index"), + } + + // Make the DynamoDB Query API call + result, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving user by user_gitlab_id: %d, error: %+v", gitlabID, err) + return nil, err + } + + // The user model + var dbUserModels []DBUser + + err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &dbUserModels) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error unmarshalling user record from database for user_gitlab_id: %d, error: %+v", gitlabID, err) + return nil, err + } + + if len(dbUserModels) == 0 { + return nil, errors.NotFound("user not found when searching by user_gitlab_id: %s", gitlabID) + } else if len(dbUserModels) > 1 { + log.WithFields(f).WithError(err).Warnf("retrieved %d results for the user_gitlab_id query when we should return 0 or 1", len(dbUserModels)) + } + + return convertDBUserModel(dbUserModels[0]), nil +} + +// GetUserByGitLabUsername fetches the user record by GitLab username +func (repo repository) GetUserByGitLabUsername(gitLabUsername string) (*models.User, error) { + f := logrus.Fields{ + "functionName": "users.repository.GetUserByGitLabUsername", + "gitLabUsername": gitLabUsername, + } + // This is the key we want to match + condition := expression.Key("user_gitlab_username").Equal(expression.Value(gitLabUsername)) + + // These are the columns we want returned + projection := buildUserProjection() + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(projection).Build() + if err != nil { + log.WithFields(f).WithError(err).Warnf("error building expression for user_gitlab_username : %s, error: %v", gitLabUsername, err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.tableName), + IndexName: aws.String("gitlab-username-index"), + } + + // Make the DynamoDB Query API call + result, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving user by user_gitlab_username: %s, error: %+v", gitLabUsername, err) + return nil, err + } + + // The user model + var dbUserModels []DBUser + + err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &dbUserModels) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error unmarshalling user record from database for user_gitlab_username: %s, error: %+v", gitLabUsername, err) + return nil, err + } + + if len(dbUserModels) == 0 { + return nil, errors.NotFound("user not found when searching by user_gitlab_username: %s", gitLabUsername) + } else if len(dbUserModels) > 1 { + log.WithFields(f).WithError(err).Warnf("retrieved %d results for the user_gitlab_username query when we should return 0 or 1", len(dbUserModels)) + } + + return convertDBUserModel(dbUserModels[0]), nil +} + func (repo repository) SearchUsers(searchField string, searchTerm string, fullMatch bool) (*models.Users, error) { f := logrus.Fields{ "functionName": "users.repository.SearchUsers", @@ -810,7 +1164,6 @@ func (repo repository) SearchUsers(searchField string, searchTerm string, fullMa users = append(users, userList...) if results.LastEvaluatedKey["user_id"] != nil { - //log.Debugf("LastEvaluatedKey: %+v", result.LastEvaluatedKey["signature_id"]) lastEvaluatedKey = *results.LastEvaluatedKey["user_id"].S scanInput.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{ "user_id": { @@ -844,13 +1197,68 @@ func (repo repository) SearchUsers(searchField string, searchTerm string, fullMa } +// UpdateUserCompanyID updates the user's company ID +func (repo repository) UpdateUserCompanyID(userID, companyID, note string) error { + f := logrus.Fields{ + "functionName": "users.repository.UpdateUserCompanyID", + } + + // First, make sure the user record exists + existingUserRecord, getErr := repo.GetUser(userID) + if getErr != nil || existingUserRecord == nil { + log.WithFields(f).WithError(getErr).Warnf("unable to update user record with company ID - user record not found for user_id: %s", userID) + return getErr + } + + expressionAttributeNames := map[string]*string{ + "#CID": aws.String("user_company_id"), + } + expressionAttributeValues := map[string]*dynamodb.AttributeValue{ + ":cid": { + S: aws.String(companyID), + }, + } + updateExpression := "SET #CID = :cid" + + // If a note is provided...add it to the update + if note != "" { + noteValue := note + // Append to the note if an existing note exists + if existingUserRecord.Note != "" { + noteValue = fmt.Sprintf("%s. %s", existingUserRecord.Note, note) + } + expressionAttributeNames["#NOTE"] = aws.String("note") + expressionAttributeValues[":NOTE"] = &dynamodb.AttributeValue{S: aws.String(noteValue)} + updateExpression = updateExpression + ", #NOTE = :NOTE" + } + + input := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "user_id": {S: aws.String(userID)}, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(repo.tableName), + } + + log.WithFields(f).Debug("updating user record with company_id...") + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("unable to update user record with company ID, error: %+v", updateErr) + return updateErr + } + + return nil +} + // convertDBUserModel translates a dyanamoDB data model into a service response model func convertDBUserModel(user DBUser) *models.User { return &models.User{ UserID: user.UserID, UserExternalID: user.UserExternalID, Admin: user.Admin, - LfEmail: user.LFEmail, + LfEmail: strfmt.Email(user.LFEmail), LfUsername: user.LFUsername, DateCreated: user.DateCreated, DateModified: user.DateModified, @@ -858,8 +1266,10 @@ func convertDBUserModel(user DBUser) *models.User { Version: user.Version, Emails: user.UserEmails, GithubID: user.UserGithubID, - CompanyID: user.UserCompanyID, GithubUsername: user.UserGithubUsername, + GitlabID: user.UserGitlabID, + GitlabUsername: user.UserGitlabUsername, + CompanyID: user.UserCompanyID, Note: user.Note, } } @@ -877,6 +1287,8 @@ func buildUserProjection() expression.ProjectionBuilder { expression.Name("user_emails"), expression.Name("user_github_username"), expression.Name("user_github_id"), + expression.Name("user_gitlab_username"), + expression.Name("user_gitlab_id"), expression.Name("date_created"), expression.Name("date_modified"), expression.Name("version"), diff --git a/cla-backend-go/users/service.go b/cla-backend-go/users/service.go index 19b918628..c57e6897b 100644 --- a/cla-backend-go/users/service.go +++ b/cla-backend-go/users/service.go @@ -7,7 +7,7 @@ import ( "errors" "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" "github.com/communitybridge/easycla/cla-backend-go/user" ) @@ -15,13 +15,18 @@ import ( type Service interface { CreateUser(user *models.User, claUser *user.CLAUser) (*models.User, error) Save(user *models.UserUpdate, claUser *user.CLAUser) (*models.User, error) + UpdateUser(userID string, updates map[string]interface{}) (*models.User, error) Delete(userID string, claUser *user.CLAUser) error GetUser(userID string) (*models.User, error) GetUserByLFUserName(lfUserName string) (*models.User, error) GetUserByUserName(userName string, fullMatch bool) (*models.User, error) GetUserByEmail(userEmail string) (*models.User, error) - GetUserByGitHubUsername(gitHubUsername string) (*models.User, error) + GetUserByGitHubID(gitHubID string) (*models.User, error) + GetUserByGitHubUsername(gitlabUsername string) (*models.User, error) + GetUserByGitlabID(gitHubID int) (*models.User, error) + GetUserByGitLabUsername(gitlabUsername string) (*models.User, error) SearchUsers(field string, searchTerm string, fullMatch bool) (*models.Users, error) + UpdateUserCompanyID(userID, companyID, note string) error } type service struct { @@ -46,7 +51,7 @@ func (s service) CreateUser(user *models.User, claUser *user.CLAUser) (*models.U // System may need to create user accounts var lfUser = "easycla_system_user" - if claUser != nil { + if claUser != nil && claUser.LFUsername != "" { lfUser = claUser.LFUsername } @@ -61,6 +66,22 @@ func (s service) CreateUser(user *models.User, claUser *user.CLAUser) (*models.U return userModel, nil } +func (s service) UpdateUser(userID string, updates map[string]interface{}) (*models.User, error) { + userModel, err := s.repo.UpdateUser(userID, updates) + if err != nil { + return nil, err + } + + // Log the event + s.events.LogEvent(&events.LogEventArgs{ + EventType: events.UserUpdated, + UserID: userID, + EventData: &events.UserUpdatedEventData{}, + }) + + return userModel, nil +} + // Save saves/updates the user record func (s service) Save(user *models.UserUpdate, claUser *user.CLAUser) (*models.User, error) { userModel, err := s.repo.Save(user) @@ -81,6 +102,9 @@ func (s service) Save(user *models.UserUpdate, claUser *user.CLAUser) (*models.U // Delete deletes the user record func (s service) Delete(userID string, claUser *user.CLAUser) error { + if userID == "" { + return errors.New("userID is empty") + } err := s.repo.Delete(userID) if err != nil { return err @@ -100,15 +124,13 @@ func (s service) Delete(userID string, claUser *user.CLAUser) error { // GetUser attempts to locate the user by the user id field func (s service) GetUser(userID string) (*models.User, error) { - userModel, err := s.repo.GetUser(userID) - if err != nil { - return nil, err + if userID == "" { + return nil, errors.New("userID is empty") } - - return userModel, nil + return s.repo.GetUser(userID) } -// GetuserByLFUserName returns the user record associated with the LF Username value +// GetUserByLFUserName returns the user record associated with the LF Username value func (s service) GetUserByLFUserName(lfUserName string) (*models.User, error) { if lfUserName == "" { return nil, errors.New("username is empty") @@ -118,40 +140,55 @@ func (s service) GetUserByLFUserName(lfUserName string) (*models.User, error) { // GetUserByUserName attempts to locate the user by the user name field func (s service) GetUserByUserName(userName string, fullMatch bool) (*models.User, error) { - userModel, err := s.repo.GetUserByUserName(userName, fullMatch) - if err != nil { - return nil, err + if userName == "" { + return nil, errors.New("username is empty") } - - return userModel, nil + return s.repo.GetUserByUserName(userName, fullMatch) } // GetUserByEmail fetches the user by email func (s service) GetUserByEmail(userEmail string) (*models.User, error) { - userModel, err := s.repo.GetUserByEmail(userEmail) - if err != nil { - return nil, err + if userEmail == "" { + return nil, errors.New("userEmail is empty") } + return s.repo.GetUserByEmail(userEmail) +} - return userModel, nil +// GetUserByGitHubID fetches the user by GitHub ID +func (s service) GetUserByGitHubID(gitHubID string) (*models.User, error) { + if gitHubID == "" { + return nil, errors.New("gitHubID is empty") + } + return s.repo.GetUserByGitHubID(gitHubID) } // GetUserByGitHubUsername fetches the user by GitHub username func (s service) GetUserByGitHubUsername(gitHubUsername string) (*models.User, error) { - userModel, err := s.repo.GetUserByGitHubUsername(gitHubUsername) - if err != nil { - return nil, err + if gitHubUsername == "" { + return nil, errors.New("gitHubUsername is empty") } + return s.repo.GetUserByGitHubUsername(gitHubUsername) +} - return userModel, nil +// GetUserByGitlabID fetches the user by Gitlab ID +func (s service) GetUserByGitlabID(gitlabID int) (*models.User, error) { + return s.repo.GetUserByGitlabID(gitlabID) +} + +// GetUserByGitLabUsername fetches the user by GitLab username +func (s service) GetUserByGitLabUsername(gitLabUsername string) (*models.User, error) { + if gitLabUsername == "" { + return nil, errors.New("gitLabUsername is empty") + } + return s.repo.GetUserByGitLabUsername(gitLabUsername) } // SearchUsers attempts to locate the user by the searchField and searchTerm fields func (s service) SearchUsers(searchField string, searchTerm string, fullMatch bool) (*models.Users, error) { - userModel, err := s.repo.SearchUsers(searchField, searchTerm, fullMatch) - if err != nil { - return nil, err - } + return s.repo.SearchUsers(searchField, searchTerm, fullMatch) +} - return userModel, nil +// UpdateUserCompanyID updates the user's company ID +func (s service) UpdateUserCompanyID(userID, companyID, note string) error { + return s.repo.UpdateUserCompanyID(userID, companyID, note) } diff --git a/cla-backend-go/utils/auth_user.go b/cla-backend-go/utils/auth_user.go index 471ffe32b..4982ca43f 100644 --- a/cla-backend-go/utils/auth_user.go +++ b/cla-backend-go/utils/auth_user.go @@ -4,6 +4,9 @@ package utils import ( + "os" + "strconv" + "github.com/LF-Engineering/lfx-kit/auth" log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/sirupsen/logrus" @@ -24,5 +27,8 @@ func SetAuthUserProperties(authUser *auth.User, xUserName *string, xEmail *strin authUser.Email = *xEmail } - log.WithFields(f).Debugf("set authuser x-username:%s and x-email:%s", authUser.UserName, authUser.Email) + tracingEnabled, conversionErr := strconv.ParseBool(os.Getenv("USER_AUTH_TRACING")) + if conversionErr == nil && tracingEnabled { + log.WithFields(f).Debugf("set authuser x-username: %s and x-email: %s from Auth User model", authUser.UserName, authUser.Email) + } } diff --git a/cla-backend-go/utils/autoenable.go b/cla-backend-go/utils/autoenable.go index a3283bfee..478019ba7 100644 --- a/cla-backend-go/utils/autoenable.go +++ b/cla-backend-go/utils/autoenable.go @@ -4,8 +4,8 @@ package utils // ValidateAutoEnabledClaGroupID checks for validation if autoEnabled flag is on autoEnabledClaGroupID is enabled as well -func ValidateAutoEnabledClaGroupID(autoEnabled *bool, autoEnabledClaGroupID string) bool { - if autoEnabled == nil || !*autoEnabled { +func ValidateAutoEnabledClaGroupID(autoEnabled bool, autoEnabledClaGroupID string) bool { + if !autoEnabled { return true } diff --git a/cla-backend-go/utils/cla_user.go b/cla-backend-go/utils/cla_user.go new file mode 100644 index 000000000..3f9fe4543 --- /dev/null +++ b/cla-backend-go/utils/cla_user.go @@ -0,0 +1,42 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" +) + +// GetBestUsername gets best username of CLA User +func GetBestUsername(user *models.User) string { + if user.Username != "" { + return user.Username + } + + if user.GithubUsername != "" { + return user.GithubUsername + } + + if user.LfUsername != "" { + return user.LfUsername + } + + return "User Name Unknown" +} + +// GetBestEmail is a helper function to return the best email address for the user model +func GetBestEmail(userModel *models.User) string { + if userModel.LfEmail != "" { + return userModel.LfEmail.String() + } + + for _, email := range userModel.Emails { + if email != "" && !strings.Contains(email, "noreply.github.com") { + return email + } + } + + return "" +} diff --git a/cla-backend-go/utils/const.go b/cla-backend-go/utils/const.go new file mode 100644 index 000000000..bc69f3844 --- /dev/null +++ b/cla-backend-go/utils/const.go @@ -0,0 +1,15 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +const ( + // Connected status + Connected = "connected" + // PartialConnection status + PartialConnection = "partial_connection" + // ConnectionFailure status + ConnectionFailure = "connection_failure" + // NoConnection status + NoConnection = "no_connection" +) diff --git a/cla-backend-go/utils/constants.go b/cla-backend-go/utils/constants.go index 37b3b9caf..8c5034ccf 100644 --- a/cla-backend-go/utils/constants.go +++ b/cla-backend-go/utils/constants.go @@ -6,7 +6,10 @@ package utils // String400 string version of 400 - http bad request const String400 = "400" -// String403 string version of 403 - http not authorized +// String401 string version of 401 - http unauthorized +const String401 = "401" + +// String403 string version of 403 - http forbidden const String403 = "403" // String404 string version of 404 - http not found @@ -21,21 +24,33 @@ const String500 = "500" // EasyCLA400BadRequest common string for handler bad request error messages const EasyCLA400BadRequest = "EasyCLA - 400 Bad Request" +// EasyCLA401Unauthorized common string for handler unauthorized error messages +const EasyCLA401Unauthorized = "EasyCLA - 401 Unauthorized" + // EasyCLA403Forbidden common string for handler forbidden error messages const EasyCLA403Forbidden = "EasyCLA - 403 Forbidden" // EasyCLA404NotFound common string for handler not found error messages const EasyCLA404NotFound = "EasyCLA - 404 Not Found" +// EasyCLA409Conflict common string for handler conflict error messages +const EasyCLA409Conflict = "EasyCLA - 409 Conflict" + // EasyCLA500InternalServerError common string for handler internal server error messages const EasyCLA500InternalServerError = "EasyCLA - 500 Internal Server Error" // GitHubBotName is the name of the GitHub bot const GitHubBotName = "EasyCLA" +// GithubBranchProtectionPatternAll is Github Branch Protection Pattern that matches all branches +const GithubBranchProtectionPatternAll = "**/**" + // TheLinuxFoundation is the name of the super parent for many Salesforce Foundations/Project Groups const TheLinuxFoundation = "The Linux Foundation" +// LFProjectsLLC is the LF project LLC name of the super parent for many Salesforce Foundations/Project Groups +const LFProjectsLLC = "LF Projects, LLC" + // ProjectUnfunded is a constant that represents a SF project that is unfunded const ProjectUnfunded = "Unfunded" @@ -45,6 +60,9 @@ const ProjectFundedSupportedByParent = "Supported by Parent Project" // XREQUESTID is the client request id - used to trace a client request through the system/logs const XREQUESTID = "x-request-id" +// CtxAuthUser the key for the authenticated user in the context +const CtxAuthUser = "authUser" + // CLAProjectManagerRole CLA project manager role identifier const CLAProjectManagerRole = "project-manager" @@ -63,7 +81,7 @@ const CLASignatoryRole = "cla-signatory" // Lead representing type of user const Lead = "lead" -//ContactRole contact role for user +// ContactRole contact role for user const ContactRole = "contact" // ProjectScope is the ACS project scope @@ -87,6 +105,12 @@ const SignatureTypeCLA = "cla" // SignatureTypeCCLA is the ccla signature type in the DB const SignatureTypeCCLA = "ccla" +// FileTypePDF is the pdf file type +const FileTypePDF = "pdf" + +// FileTypeCSV is the csv file type +const FileTypeCSV = "csv" + // SignatureReferenceTypeUser is the signature reference type for user signatures - individual and employee const SignatureReferenceTypeUser = "user" @@ -111,11 +135,104 @@ const SortOrderDescending = "desc" // RecordDeleted dynamo event for deleting a record const RecordDeleted = "REMOVE" -//RecordModified dynamo event on modifying a record +// RecordModified dynamo event on modifying a record const RecordModified = "MODIFY" -//RecordAdded dynami event on adding a record +// RecordAdded dynami event on adding a record const RecordAdded = "INSERT" -//GithubRepoNotFound representing not found repos for CLAGroupID -const GithubRepoNotFound = "github repository not found" +// GithubRepoNotFound is a string that indicates the GitHub repository is not found +const GithubRepoNotFound = "GitHub repository not found" + +// GithubRepoExists is a string that indicates the GitHub repository already exists +const GithubRepoExists = "GitHub repository exists" + +// GitHubEmailLabel represents the GH Email label used for email +const GitHubEmailLabel = "GitHub Email Address" + +// GitHubUserLabel represents the GH username Label used for email +const GitHubUserLabel = "GitHub Username" + +// GitLab is the GitLab spelled out with the proper case +const GitLab = "GitLab" + +// GitLabLower is the GitLab spelled out in lower case +const GitLabLower = "gitlab" + +// GitLabRepoNotFound is a string that indicates the GitLab repository is not found +const GitLabRepoNotFound = "GitLab repository not found" + +// GitLabDuplicateRepoFound is a string that indicates that duplicate GitLab repositories were found +const GitLabDuplicateRepoFound = "Duplicate GitLab repositories were found" + +// GitLabRepoExists is a string that indicates the GitLab repository already exists +const GitLabRepoExists = "GitLab repository exists" + +// GitLabEmailLabel represents the GitLab Email label used for email +const GitLabEmailLabel = "GitLab Email Address" + +// GitLabUserLabel represents the GitLab username Label used for email +const GitLabUserLabel = "GitLab Username" + +// EmailLabel represents LF/EasyCLA Email address +const EmailLabel = "Email Address" + +// UserLabel represents the LF/EasyCLA username +const UserLabel = "Username" + +// EmailDomainCriteria represents approval based on email domain +const EmailDomainCriteria = "Email Domain Criteria" + +// EmailCriteria represents approvals based on email addresses +const EmailCriteria = "Email Criteria" + +// AddApprovals is an action for adding approvals +const AddApprovals = "AddApprovals" + +// RemoveApprovals is an action for removing approvals +const RemoveApprovals = "RemoveApprovals" + +// GitHubUsernameCriteria represents criteria based on GitHub username +const GitHubUsernameCriteria = "GitHubUsername" + +// GitHubOrgCriteria represents approvals based on GitHub org membership +const GitHubOrgCriteria = "GitHub Org Criteria" + +// GitlabUsernameCriteria represents criteria based on gitlab username +const GitlabUsernameCriteria = "GitHubUsername" + +// GitlabOrgCriteria represents approvals based on gitlab org group membership +const GitlabOrgCriteria = "Gitlab Org Criteria" + +// SignatureQueryDefaultAll the signature query default active value - 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 +const SignatureQueryDefaultAll = "all" + +// SignatureQueryDefaultActive the signature query default active value - 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 +const SignatureQueryDefaultActive = "active" + +// GitLabRepositoryType representing the GitLab repository type +const GitLabRepositoryType = "GitLab" + +// GitHubRepositoryType representing the GitLab repository type +const GitHubRepositoryType = "GitHub" + +// ContextKey is the key for the context +type contextKey string + +const XREQUESTIDKey contextKey = "x-request-id" + +const GithubUsernameApprovalCriteria = "githubUsername" + +const GithubOrgApprovalCriteria = "githubOrg" + +const GitlabUsernameApprovalCriteria = "gitlabUsername" + +const GitlabOrgApprovalCriteria = "gitlabOrg" + +const EmailApprovalCriteria = "email" + +const DomainApprovalCriteria = "domain" diff --git a/cla-backend-go/utils/context.go b/cla-backend-go/utils/context.go index e2283ffe1..f9e092015 100644 --- a/cla-backend-go/utils/context.go +++ b/cla-backend-go/utils/context.go @@ -6,6 +6,8 @@ package utils import ( "context" + "github.com/LF-Engineering/lfx-kit/auth" + "github.com/sirupsen/logrus" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -25,3 +27,67 @@ func NewContext() context.Context { return context.WithValue(context.Background(), XREQUESTID, requestID.String()) // nolint } + +// NewContextWithUser returns a new context with a newly generated request ID and the specified user +func NewContextWithUser(authUser *auth.User) context.Context { + f := logrus.Fields{ + "functionName": "utils.NewContextWithUser", + } + requestID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to generate a UUID for x-request-id") + return context.Background() + } + + return context.WithValue(context.WithValue(context.Background(), XREQUESTID, requestID), CtxAuthUser, authUser) // nolint +} + +// NewContextFromParent returns a new context object with a new request ID based on the parent +func NewContextFromParent(ctx context.Context) context.Context { + f := logrus.Fields{ + "functionName": "utils.NewContext", + } + requestID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to generate a UUID for x-request-id") + return context.Background() + } + + return context.WithValue(ctx, XREQUESTID, requestID.String()) // nolint +} + +// ContextWithRequestAndUser returns a new context with the specified request ID and user +func ContextWithRequestAndUser(ctx context.Context, reqID string, authUser *auth.User) context.Context { + return context.WithValue(context.WithValue(ctx, XREQUESTID, reqID), CtxAuthUser, authUser) // nolint +} + +// ContextWithUser returns a new context with the specified user +func ContextWithUser(ctx context.Context, authUser *auth.User) context.Context { + return context.WithValue(ctx, "authUser", authUser) // nolint +} + +// GetUserNameFromContext returns the user's name from the context +func GetUserNameFromContext(ctx context.Context) string { + val := ctx.Value(CtxAuthUser) + if val != nil { + authUser := val.(*auth.User) // nolint + if authUser != nil { + return authUser.UserName + } + } + + return "" +} + +// GetUserEmailFromContext returns the user's email from the context +func GetUserEmailFromContext(ctx context.Context) string { + val := ctx.Value(CtxAuthUser) + if val != nil { + authUser := val.(*auth.User) // nolint + if authUser != nil { + return authUser.Email + } + } + + return "" +} diff --git a/cla-backend-go/utils/conversion.go b/cla-backend-go/utils/conversion.go index 10f56725b..f98ab0c7e 100644 --- a/cla-backend-go/utils/conversion.go +++ b/cla-backend-go/utils/conversion.go @@ -11,6 +11,11 @@ func StringValue(input *string) string { return *input } +// StringRef function convert string to string reference +func StringRef(input string) *string { + return &input +} + // Int64Value function convert int64 pointer to string func Int64Value(input *int64) int64 { if input == nil { @@ -31,3 +36,27 @@ func BoolValue(input *bool) bool { } return *input } + +// Bool function convert boolean to boolean pointer +func Bool(input bool) *bool { + return &input +} + +// GetNilSliceIfEmpty returns a nil reference is the specified slice is empty, otherwise returns a reference to the original slice +func GetNilSliceIfEmpty(slice []string) []string { + if len(slice) == 0 { + return nil + } + + return slice +} + +// ArrayStringPointer converts Array string to Array string pointer +func ArrayStringPointer(input []string) []*string { + var conversion []*string + for _, v := range input { + v2 := v + conversion = append(conversion, &v2) + } + return conversion +} diff --git a/cla-backend-go/utils/email.go b/cla-backend-go/utils/email.go index 9333b7827..fe3749d7f 100644 --- a/cla-backend-go/utils/email.go +++ b/cla-backend-go/utils/email.go @@ -4,8 +4,9 @@ package utils import ( + "bytes" "errors" - "fmt" + "html/template" "strings" "github.com/sirupsen/logrus" @@ -74,7 +75,7 @@ func (s *snsEmail) SendEmail(subject string, body string, recipients []string) e return err } - log.Debugf("Sending SNS message '%s' to topic: '%s'", b, s.snsEventTopicARN) + log.Debugf("Sending SNS message to topic: '%s'", s.snsEventTopicARN) input := &sns.PublishInput{ Message: aws.String(string(b)), // Required TopicArn: aws.String(s.snsEventTopicARN), // Required @@ -86,7 +87,7 @@ func (s *snsEmail) SendEmail(subject string, body string, recipients []string) e return err } - log.WithFields(f).Debugf("Successfully sent SNS message. Response: %v", sendResp) + log.WithFields(f).Debugf("Successfully sent SNS message. Response ID: %s", StringValue(sendResp.MessageId)) return nil } @@ -103,26 +104,43 @@ func GetCorporateURL(isV2Project bool) string { if isV2Project { return config.GetConfig().CorporateConsoleV2URL } - return fmt.Sprintf("https://%s", config.GetConfig().CorporateConsoleURL) + return config.GetConfig().CorporateConsoleV1URL } // GetEmailHelpContent returns the standard email help paragraph details. func GetEmailHelpContent(showV2HelpLink bool) string { - if showV2HelpLink { - return `

    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////.pdf +// claType should be cla or ccla +// identifier can be user-id or company-id +func UploadFileToS3(file *os.File, projectID string, claType string, identifier string, signatureID string) error { + if s3Storage == nil { + return errors.New("s3Storage not set") + } + + return s3Storage.UploadFile(file, projectID, claType, identifier, signatureID) +} + +func DocumentExists(key string) (bool, error) { + if s3Storage == nil { + return false, errors.New("s3 storage not set") + } + return s3Storage.KeyExists(key) +} + // DownloadFromS3 downloads file from s3 func DownloadFromS3(filename string) ([]byte, error) { if s3Storage == nil { @@ -135,6 +168,35 @@ func GetDownloadLink(filename string) (string, error) { return s3Storage.GetPresignedURL(filename) } +// KeyExists checks if key exists in s3 +func (s3c *S3Client) KeyExists(key string) (bool, error) { + f := logrus.Fields{ + "functionName": "utils.s3.KeyExists", + "bucketName": s3c.BucketName, + "key": key, + } + + log.WithFields(f).Debug("checking for key") + + _, err := s3c.s3.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String(s3c.BucketName), + Key: aws.String(key), + }) + if err != nil { + // check for NotFound error + var nsk *types.NoSuchKey + if errors.As(err, &nsk) || strings.Contains(err.Error(), "NotFound") { + log.WithFields(f).Debug("key not found") + return false, nil + } + log.WithFields(f).WithError(err).Warn("problem checking for key") + return false, err + } + + log.WithFields(f).Debugf("s3 document exists for key: %s", key) + return true, nil +} + // SignedCLAFilename provide s3 bucket url func SignedCLAFilename(projectID string, claType string, identifier string, signatureID string) string { return strings.Join([]string{"contract-group", projectID, claType, identifier, signatureID}, "/") + ".pdf" diff --git a/cla-backend-go/utils/signature_utils.go b/cla-backend-go/utils/signature_utils.go index 1f9f9b30c..5b7612f5c 100644 --- a/cla-backend-go/utils/signature_utils.go +++ b/cla-backend-go/utils/signature_utils.go @@ -5,17 +5,11 @@ package utils import ( "github.com/LF-Engineering/lfx-kit/auth" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" - log "github.com/communitybridge/easycla/cla-backend-go/logging" - "github.com/sirupsen/logrus" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" ) // CurrentUserInACL is a helper function to determine if the current logged in user is in the specified CLA Manager list func CurrentUserInACL(authUser *auth.User, managers []v1Models.User) bool { - f := logrus.Fields{ - "functionName": "utils.CurrentUserInACL", - } - log.WithFields(f).Debugf("checking if user: %+v is in the Signature ACL: %+v", authUser, managers) var inACL = false for _, manager := range managers { if manager.LfUsername == authUser.UserName { @@ -24,6 +18,5 @@ func CurrentUserInACL(authUser *auth.User, managers []v1Models.User) bool { } } - log.WithFields(f).Debugf("user in acl: %t", inACL) return inACL } diff --git a/cla-backend-go/utils/string_utils.go b/cla-backend-go/utils/string_utils.go index 855c43785..934550ba2 100644 --- a/cla-backend-go/utils/string_utils.go +++ b/cla-backend-go/utils/string_utils.go @@ -24,3 +24,19 @@ func TrimSpaceFromItems(arr []string) []string { return newArr } + +// GetFirstAndLastName parses the user's name into first and last strings +func GetFirstAndLastName(firstAndLastName string) (string, string) { + // Parse the provided user's name + userNames := strings.Split(firstAndLastName, " ") + var userFirstName string + var userLastName string + if len(userNames) >= 2 { + userFirstName = userNames[0] + userLastName = userNames[len(userNames)-1] + } else if len(userNames) == 1 { + userFirstName = userNames[0] + } + + return strings.TrimSpace(userFirstName), strings.TrimSpace(userLastName) +} diff --git a/cla-backend-go/utils/utils.go b/cla-backend-go/utils/utils.go index fbfb18feb..ce817a12f 100644 --- a/cla-backend-go/utils/utils.go +++ b/cla-backend-go/utils/utils.go @@ -6,13 +6,12 @@ package utils import ( "fmt" "math" - "regexp" "strconv" "strings" "time" - "unicode/utf8" - "github.com/gofrs/uuid" + 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/service/dynamodb" @@ -40,6 +39,22 @@ func TimeToString(t time.Time) string { return t.UTC().Format(time.RFC3339) } +// FormatTimeString converts the time string into the "standard" RFC3339 format +func FormatTimeString(timeStr string) string { + f := logrus.Fields{ + "functionName": "utils.utils.FormatTimeString", + "timeStr": timeStr, + } + + t, err := ParseDateTime(timeStr) + if err != nil { + log.WithFields(f).Warnf("unable to convert the time string: %s into a standard form.", timeStr) + return timeStr + } + + return t.UTC().Format(time.RFC3339) +} + // CurrentTime returns the current UTC time and current Time in the RFC3339 format func CurrentTime() (time.Time, string) { t := time.Now().UTC() @@ -59,10 +74,17 @@ func ParseDateTime(dateTimeStr string) (time.Time, error) { supportedFormats := []string{ time.RFC3339, time.RFC3339Nano, + "2006-01-02T15:04:05", "2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z-07:00", "2006-01-02T15:04:05-07:00", "2006-01-02T15:04:05.000000-0700", + "2006-01-02T15:04:05.0", + "2006-01-02T15:04:05.00", + "2006-01-02T15:04:05.000", + "2006-01-02T15:04:05.0000", + "2006-01-02T15:04:05.00000", + "2006-01-02T15:04:05.000000", time.RFC850, time.RFC1123, time.RFC1123Z, @@ -178,108 +200,3 @@ func SliceDifference(a, b []string) []string { } return diff } - -// ValidEmail tests the specified email string, returns true if email is valid, returns false otherwise -func ValidEmail(email string) bool { - emailRegexp := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - return emailRegexp.MatchString(strings.TrimSpace(email)) -} - -// ValidDomain tests the specified domain string, returns true if domain is valid, returns false otherwise -func ValidDomain(domain string) (string, bool) { - domain = strings.TrimSpace(domain) - - switch { - case len(domain) == 0: - return "domain is empty", false - case len(domain) > 255: - return fmt.Sprintf("domain name length is %d, can't exceed 255", len(domain)), false - } - var l int - for i := 0; i < len(domain); i++ { - b := domain[i] - if b == '.' { - // check domain labels validity - switch { - case i == l: - return fmt.Sprintf("invalid character '%c' at offset %d: label can't begin with a period", b, i), false - case i-l > 63: - return fmt.Sprintf("byte length of label '%s' is %d, can't exceed 63", domain[l:i], i-l), false - case domain[l] == '-': - return fmt.Sprintf("label '%s' at offset %d begins with a hyphen", domain[l:i], l), false - case domain[i-1] == '-': - return fmt.Sprintf("label '%s' at offset %d ends with a hyphen", domain[l:i], l), false - } - l = i + 1 - continue - } - // test label character validity, note: tests are ordered by decreasing validity frequency - if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b >= 'A' && b <= 'Z') { - // show the printable unicode character starting at byte offset i - c, _ := utf8.DecodeRuneInString(domain[i:]) - if c == utf8.RuneError { - return fmt.Sprintf("invalid character at offset %d", i), false - } - return fmt.Sprintf("invalid character '%c' at offset %d", c, i), false - } - } - - // check top level domain validity - switch { - case l == len(domain): - return "missing top level domain, domain can't end with a period", false - case len(domain)-l > 63: - return fmt.Sprintf("byte length of top level domain '%s' is %d, can't exceed 63", domain[l:], len(domain)-l), false - case domain[l] == '-': - return fmt.Sprintf("top level domain '%s' at offset %d begins with a hyphen", domain[l:], l), false - case domain[len(domain)-1] == '-': - return fmt.Sprintf("top level domain '%s' at offset %d ends with a hyphen", domain[l:], l), false - case domain[l] >= '0' && domain[l] <= '9': - return fmt.Sprintf("top level domain '%s' at offset %d begins with a digit", domain[l:], l), false - } - - return "", true -} - -// ValidGitHubUsername tests the specified GitHub username string, returns true if valid, returns false otherwise -func ValidGitHubUsername(githubUsername string) (string, bool) { - - if len(strings.TrimSpace(githubUsername)) <= 2 { - return "github username must be 3 or more characters", false - } - - // For now, we only allow alpha numeric values - re := regexp.MustCompile("^[a-zA-Z0-9_-]*$") - valid := re.MatchString(strings.TrimSpace(githubUsername)) - if !valid { - return fmt.Sprintf("invalid GitHub username: %s", githubUsername), false - } - - return "", true -} - -// ValidGitHubOrg tests the specified GitHub Organization string, returns true if valid, returns false otherwise -func ValidGitHubOrg(githubOrg string) (string, bool) { - - if len(strings.TrimSpace(githubOrg)) <= 2 { - return "github organization must be 3 or more characters", false - } - - re := regexp.MustCompile("^[a-zA-Z0-9._-]*$") - valid := re.MatchString(strings.TrimSpace(githubOrg)) - if !valid { - return fmt.Sprintf("invalid GitHub organization: %s", githubOrg), false - } - - return "", true -} - -// IsUUIDv4 returns true if the specified ID is in the UUIDv4 format, otherwise returns false -func IsUUIDv4(id string) bool { - value, err := uuid.FromString(id) - if err != nil { - return false - } - - return value.Version() == uuid.V4 -} diff --git a/cla-backend-go/utils/utils_user_auth_lambda.go b/cla-backend-go/utils/utils_user_auth_lambda.go index 007687d37..d5dfdf82a 100644 --- a/cla-backend-go/utils/utils_user_auth_lambda.go +++ b/cla-backend-go/utils/utils_user_auth_lambda.go @@ -1,3 +1,4 @@ +//go:build aws_lambda // +build aws_lambda // Copyright The Linux Foundation and each contributor to CommunityBridge. @@ -6,7 +7,14 @@ package utils import ( + "context" + "fmt" + "strings" + + "github.com/sirupsen/logrus" + "github.com/LF-Engineering/lfx-kit/auth" + log "github.com/communitybridge/easycla/cla-backend-go/logging" ) const ( @@ -22,79 +30,239 @@ func IsUserAdmin(user *auth.User) bool { } // IsUserAuthorizedForOrganization helper function for determining if the user is authorized for this company -func IsUserAuthorizedForOrganization(user *auth.User, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForOrganization(ctx context.Context, user *auth.User, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForOrganization", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - return user.IsUserAuthorizedForOrganizationScope(companySFID) + val := user.IsUserAuthorizedForOrganizationScope(companySFID) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for companySFID: %s admin flag=%t", + user.UserName, companySFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for companySFID: %s, admin flag=%t, scopeInfo: %s", + user.UserName, companySFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForProjectTree helper function for determining if the user is authorized for this project hierarchy/tree -func IsUserAuthorizedForProjectTree(user *auth.User, projectSFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProjectTree(ctx context.Context, user *auth.User, projectSFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProjectTree", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "adminScopeAllowed": adminScopeAllowed, + } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - return user.IsUserAuthorized(auth.Project, projectSFID, true) + log.WithFields(f).Debugf("checking project scope for projectSFID: %s...", projectSFID) + val := user.IsUserAuthorized(auth.Project, projectSFID, true) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s tree, admin flag=%t", + user.UserName, projectSFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s tree, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForProject helper function for determining if the user is authorized for this project -func IsUserAuthorizedForProject(user *auth.User, projectSFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProject(ctx context.Context, user *auth.User, projectSFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProject", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "adminScopeAllowed": adminScopeAllowed, + } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - return user.IsUserAuthorizedForProjectScope(projectSFID) + log.WithFields(f).Debugf("checking project scope for projectSFID: %s...", projectSFID) + val := user.IsUserAuthorizedForProjectScope(projectSFID) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s, admin flag=%t", + user.UserName, projectSFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForAnyProjects helper function for determining if the user is authorized for any of the specified projects -func IsUserAuthorizedForAnyProjects(user *auth.User, projectSFIDs []string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForAnyProjects(ctx context.Context, user *auth.User, projectSFIDs []string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForAnyProjects", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFIDs": strings.Join(projectSFIDs, ","), + "adminScopeAllowed": adminScopeAllowed, + } + for _, projectSFID := range projectSFIDs { - if IsUserAuthorizedForProjectTree(user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("checking project tree scope for: %s...", projectSFID) + if IsUserAuthorizedForProjectTree(ctx, user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("project tree scope check passed for: %s...", projectSFID) return true } - if IsUserAuthorizedForProject(user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("checking project scope for: %s...", projectSFID) + if IsUserAuthorizedForProject(ctx, user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("project scope check passed for: %s...", projectSFID) return true } } + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for project scope checks for any projects: %s, admin flag=%t, scopeInfo: %s", + user.UserName, strings.Join(projectSFIDs, ","), user.Admin, scopeInfo) return false } // IsUserAuthorizedForProjectOrganization helper function for determining if the user is authorized for this project organization scope -func IsUserAuthorizedForProjectOrganization(user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProjectOrganization(ctx context.Context, user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProjectOrganization", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - return user.IsUserAuthorizedByProject(projectSFID, companySFID) + val := user.IsUserAuthorizedByProject(projectSFID, companySFID) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t", + user.UserName, projectSFID, companySFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, companySFID, user.Admin, scopeInfo) + } + + return val } // IsUserAuthorizedForAnyProjectOrganization helper function for determining if the user is authorized for any of the specified projects with scope of project + organization -func IsUserAuthorizedForAnyProjectOrganization(user *auth.User, projectSFIDs []string, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForAnyProjectOrganization(ctx context.Context, user *auth.User, projectSFIDs []string, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForAnyProjectOrganization", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFIDs": strings.Join(projectSFIDs, ","), + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } + + if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") + return true + } + for _, projectSFID := range projectSFIDs { - if IsUserAuthorizedForProjectOrganizationTree(user, projectSFID, companySFID, adminScopeAllowed) { + if IsUserAuthorizedForProjectOrganizationTree(ctx, user, projectSFID, companySFID, adminScopeAllowed) { + log.WithFields(f).Debugf("user is authorized for projectSFID: %s + companySFID: %s tree", projectSFID, companySFID) return true } - if IsUserAuthorizedForProjectOrganization(user, projectSFID, companySFID, adminScopeAllowed) { + if IsUserAuthorizedForProjectOrganization(ctx, user, projectSFID, companySFID, adminScopeAllowed) { + log.WithFields(f).Debugf("user is authorized for projectSFID: %s + companySFID: %s", projectSFID, companySFID) return true } } + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for any projectSFID: %s + companySFID: %s, admin flag=%t, scopeInfo: %s", + user.UserName, strings.Join(projectSFIDs, ","), companySFID, user.Admin, scopeInfo) return false } // IsUserAuthorizedForProjectOrganizationTree helper function for determining if the user is authorized for this project organization scope and nested projects/orgs -func IsUserAuthorizedForProjectOrganizationTree(user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProjectOrganizationTree(ctx context.Context, user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProjectOrganizationTree", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - return user.IsUserAuthorized(auth.ProjectOrganization, projectSFID+"|"+companySFID, true) + val := user.IsUserAuthorized(auth.ProjectOrganization, projectSFID+"|"+companySFID, true) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t", + user.UserName, projectSFID, companySFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, companySFID, user.Admin, scopeInfo) + } + + return val } diff --git a/cla-backend-go/utils/utils_user_auth_standalone.go b/cla-backend-go/utils/utils_user_auth_standalone.go index a66b2fb81..aa4e1dd43 100644 --- a/cla-backend-go/utils/utils_user_auth_standalone.go +++ b/cla-backend-go/utils/utils_user_auth_standalone.go @@ -1,3 +1,4 @@ +//go:build !aws_lambda // +build !aws_lambda // Copyright The Linux Foundation and each contributor to CommunityBridge. @@ -6,8 +7,13 @@ package utils import ( + "context" + "fmt" "os" "strconv" + "strings" + + "github.com/sirupsen/logrus" "github.com/LF-Engineering/lfx-kit/auth" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -42,116 +48,280 @@ func skipPermissionChecks() bool { } // IsUserAuthorizedForOrganization helper function for determining if the user is authorized for this company -func IsUserAuthorizedForOrganization(user *auth.User, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForOrganization(ctx context.Context, user *auth.User, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForOrganization", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } + // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") return true } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - return user.IsUserAuthorizedForOrganizationScope(companySFID) + val := user.IsUserAuthorizedForOrganizationScope(companySFID) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for companySFID: %s admin flag=%t", + user.UserName, companySFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for companySFID: %s, admin flag=%t, scopeInfo: %s", + user.UserName, companySFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForProjectTree helper function for determining if the user is authorized for this project hierarchy/tree -func IsUserAuthorizedForProjectTree(user *auth.User, projectSFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProjectTree(ctx context.Context, user *auth.User, projectSFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProjectTree", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "adminScopeAllowed": adminScopeAllowed, + } + + log.WithFields(f).Debugf("checking user auth for project tree") + // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") return true } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - // Previously, we checked for user.Admin - admins should be in a separate role - // Previously, we checked for user.Allowed, which is currently not used (future flag that is currently not implemented) - return user.IsUserAuthorized(auth.Project, projectSFID, true) + log.WithFields(f).Debugf("checking project scope for projectSFID: %s...", projectSFID) + val := user.IsUserAuthorized(auth.Project, projectSFID, true) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s tree, admin flag=%t", + user.UserName, projectSFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s tree, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForProject helper function for determining if the user is authorized for this project -func IsUserAuthorizedForProject(user *auth.User, projectSFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProject(ctx context.Context, user *auth.User, projectSFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProject", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "adminScopeAllowed": adminScopeAllowed, + } + // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") return true } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - // Previously, we checked for user.Admin - admins should be in a separate role - // Previously, we checked for user.Allowed, which is currently not used (future flag that is currently not implemented) - return user.IsUserAuthorizedForProjectScope(projectSFID) + log.WithFields(f).Debugf("checking project scope for projectSFID: %s...", projectSFID) + val := user.IsUserAuthorizedForProjectScope(projectSFID) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s, admin flag=%t", + user.UserName, projectSFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForAnyProjects helper function for determining if the user is authorized for any of the specified projects -func IsUserAuthorizedForAnyProjects(user *auth.User, projectSFIDs []string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForAnyProjects(ctx context.Context, user *auth.User, projectSFIDs []string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForAnyProjects", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFIDs": strings.Join(projectSFIDs, ","), + "adminScopeAllowed": adminScopeAllowed, + } // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") return true } for _, projectSFID := range projectSFIDs { - if IsUserAuthorizedForProjectTree(user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("checking project tree scope for: %s...", projectSFID) + if IsUserAuthorizedForProjectTree(ctx, user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("project tree scope check passed for: %s...", projectSFID) return true } - if IsUserAuthorizedForProject(user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("checking project scope for: %s...", projectSFID) + if IsUserAuthorizedForProject(ctx, user, projectSFID, adminScopeAllowed) { + log.WithFields(f).Debugf("project scope check passed for: %s...", projectSFID) return true } } + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for project scope checks for any projects: %s, admin flag=%t, scopeInfo: %s", + user.UserName, strings.Join(projectSFIDs, ","), user.Admin, scopeInfo) return false } // IsUserAuthorizedForProjectOrganization helper function for determining if the user is authorized for this project organization scope -func IsUserAuthorizedForProjectOrganization(user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProjectOrganization(ctx context.Context, user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProjectOrganization", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") return true } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - // Previously, we checked for user.Allowed, which is currently not used (future flag that is currently not implemented) - return user.IsUserAuthorizedByProject(projectSFID, companySFID) + val := user.IsUserAuthorizedByProject(projectSFID, companySFID) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t", + user.UserName, projectSFID, companySFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, companySFID, user.Admin, scopeInfo) + } + return val } // IsUserAuthorizedForAnyProjectOrganization helper function for determining if the user is authorized for any of the specified projects with scope of project + organization -func IsUserAuthorizedForAnyProjectOrganization(user *auth.User, projectSFIDs []string, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForAnyProjectOrganization(ctx context.Context, user *auth.User, projectSFIDs []string, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForAnyProjectOrganization", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFIDs": strings.Join(projectSFIDs, ","), + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } + // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") + return true + } + + if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } for _, projectSFID := range projectSFIDs { - if IsUserAuthorizedForProjectOrganizationTree(user, projectSFID, companySFID, adminScopeAllowed) { + if IsUserAuthorizedForProjectOrganizationTree(ctx, user, projectSFID, companySFID, adminScopeAllowed) { + log.WithFields(f).Debugf("user is authorized for projectSFID: %s + companySFID: %s tree", projectSFID, companySFID) return true } - if IsUserAuthorizedForProjectOrganization(user, projectSFID, companySFID, adminScopeAllowed) { + if IsUserAuthorizedForProjectOrganization(ctx, user, projectSFID, companySFID, adminScopeAllowed) { + log.WithFields(f).Debugf("user is authorized for projectSFID: %s + companySFID: %s", projectSFID, companySFID) return true } } + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for any projectSFID: %s + companySFID: %s, admin flag=%t, scopeInfo: %s", + user.UserName, strings.Join(projectSFIDs, ","), companySFID, user.Admin, scopeInfo) return false } // IsUserAuthorizedForProjectOrganizationTree helper function for determining if the user is authorized for this project organization scope and nested projects/orgs -func IsUserAuthorizedForProjectOrganizationTree(user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { +func IsUserAuthorizedForProjectOrganizationTree(ctx context.Context, user *auth.User, projectSFID, companySFID string, adminScopeAllowed bool) bool { + f := logrus.Fields{ + "functionName": "utils.IsUserAuthorizedForProjectOrganizationTree", + XREQUESTID: ctx.Value(XREQUESTID), + "userName": user.UserName, + "userEmail": user.Email, + "projectSFID": projectSFID, + "companySFID": companySFID, + "adminScopeAllowed": adminScopeAllowed, + } + // If we are running locally and want to disable permission checks if skipPermissionChecks() { + log.WithFields(f).Debug("skipping permissions check") return true } if adminScopeAllowed && user.Admin { + log.WithFields(f).Debug("user is authorized - admin scope is allowed and admin scope set for user") return true } - // Previously, we checked for user.Admin - admins should be in a separate role - // Previously, we checked for user.Allowed, which is currently not used (future flag that is currently not implemented) - return user.IsUserAuthorized(auth.ProjectOrganization, projectSFID+"|"+companySFID, true) + val := user.IsUserAuthorized(auth.ProjectOrganization, projectSFID+"|"+companySFID, true) + if val { + log.WithFields(f).Debugf("user '%s' is authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t", + user.UserName, projectSFID, companySFID, user.Admin) + } else { + var scopeInfo string + for i, scope := range user.Scopes { + scopeInfo = fmt.Sprintf("%sscope[%d] = {type=%s, id=%s, level=%s, role=%s, related=[%s]} ", + scopeInfo, i, scope.Type, scope.ID, scope.Level, scope.Role, strings.Join(scope.Related, ",")) + } + log.WithFields(f).Debugf("user '%s' is not authorized for projectSFID: %s + companySFID: %s tree, admin flag=%t, scopeInfo: %s", + user.UserName, projectSFID, companySFID, user.Admin, scopeInfo) + } + + return val } diff --git a/cla-backend-go/utils/validators.go b/cla-backend-go/utils/validators.go new file mode 100644 index 000000000..bb3cc8954 --- /dev/null +++ b/cla-backend-go/utils/validators.go @@ -0,0 +1,171 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "fmt" + "regexp" + "strings" + "unicode/utf8" + + "github.com/gofrs/uuid" +) + +// ValidEmail tests the specified email string, returns true if email is valid, returns false otherwise +func ValidEmail(email string) bool { + emailRegexp := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + return emailRegexp.MatchString(strings.TrimSpace(email)) +} + +// ValidDomain tests the specified domain string, returns true if domain is valid, returns false otherwise +func ValidDomain(domain string, allowWildcard bool) (string, bool) { // nolint + domain = strings.TrimSpace(domain) + + switch { + case len(domain) == 0: + return "domain is empty", false + case len(domain) > 255: + return fmt.Sprintf("domain name length is %d, can't exceed 255", len(domain)), false + } + var l int + for i := 0; i < len(domain); i++ { + b := domain[i] + if b == '.' { + // check domain labels validity + switch { + case i == l: + return fmt.Sprintf("invalid character '%c' at offset %d: label can't begin with a period", b, i), false + case i-l > 63: + return fmt.Sprintf("byte length of label '%s' is %d, can't exceed 63", domain[l:i], i-l), false + case domain[l] == '-': + return fmt.Sprintf("label '%s' at offset %d begins with a hyphen", domain[l:i], l), false + case domain[i-1] == '-': + return fmt.Sprintf("label '%s' at offset %d ends with a hyphen", domain[l:i], l), false + } + l = i + 1 + continue + } + + // If wildcard domains are allowed, e.g. *.linuxfoundation.org + if allowWildcard { + // test label character validity, note: tests are ordered by decreasing validity frequency + if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b == '*' || b >= 'A' && b <= 'Z') { + // show the printable unicode character starting at byte offset i + c, _ := utf8.DecodeRuneInString(domain[i:]) + if c == utf8.RuneError { + return fmt.Sprintf("invalid character at offset %d", i), false + } + return fmt.Sprintf("invalid character '%c' at offset %d", c, i), false + } + } else { + // test label character validity, note: tests are ordered by decreasing validity frequency + if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b >= 'A' && b <= 'Z') { + // show the printable unicode character starting at byte offset i + c, _ := utf8.DecodeRuneInString(domain[i:]) + if c == utf8.RuneError { + return fmt.Sprintf("invalid character at offset %d", i), false + } + return fmt.Sprintf("invalid character '%c' at offset %d", c, i), false + } + } + } + + // check top level domain validity + switch { + case l == len(domain): + return "missing top level domain, domain can't end with a period", false + case len(domain)-l > 63: + return fmt.Sprintf("byte length of top level domain '%s' is %d, can't exceed 63", domain[l:], len(domain)-l), false + case domain[l] == '-': + return fmt.Sprintf("top level domain '%s' at offset %d begins with a hyphen", domain[l:], l), false + case domain[len(domain)-1] == '-': + return fmt.Sprintf("top level domain '%s' at offset %d ends with a hyphen", domain[l:], l), false + case domain[l] >= '0' && domain[l] <= '9': + return fmt.Sprintf("top level domain '%s' at offset %d begins with a digit", domain[l:], l), false + } + + return "", true +} + +// ValidGitHubUsername tests the specified GitHub username string, returns true if valid, returns false otherwise +func ValidGitHubUsername(githubUsername string) (string, bool) { + + if len(strings.TrimSpace(githubUsername)) <= 2 { + return "github username must be 3 or more characters", false + } + + // For now, we only allow alphanumeric values + re := regexp.MustCompile("^[a-zA-Z0-9._-]*$") + valid := re.MatchString(strings.TrimSpace(githubUsername)) + if !valid { + return fmt.Sprintf("invalid GitHub username: %s", githubUsername), false + } + + return "", true +} + +// ValidGitlabUsername tests the specified Gitlab username string, returns true if valid, returns false otherwise +func ValidGitlabUsername(gitlabUsername string) (string, bool) { + + if len(strings.TrimSpace(gitlabUsername)) <= 2 { + return "gitlab username must be 3 or more characters", false + } + + // For now, we only allow alphanumeric values + re := regexp.MustCompile("^[a-zA-Z0-9._-]*$") + valid := re.MatchString(strings.TrimSpace(gitlabUsername)) + if !valid { + return fmt.Sprintf("invalid Gitlab username: %s", gitlabUsername), false + } + + return "", true +} + +// ValidGitHubOrg tests the specified GitHub Organization string, returns true if valid, returns false otherwise +func ValidGitHubOrg(githubOrg string) (string, bool) { + + if len(strings.TrimSpace(githubOrg)) <= 2 { + return "github organization must be 3 or more characters", false + } + + re := regexp.MustCompile("^[a-zA-Z0-9._-]*$") + valid := re.MatchString(strings.TrimSpace(githubOrg)) + if !valid { + return fmt.Sprintf("invalid GitHub organization: %s", githubOrg), false + } + + return "", true +} + +// ValidGitlabOrg tests the specified Gitlab Organization string, returns true if valid, returns false otherwise +func ValidGitlabOrg(gitlabOrg string) (string, bool) { + + if len(strings.TrimSpace(gitlabOrg)) <= 2 { + return "gitlab organization must be 3 or more characters", false + } + + re := regexp.MustCompile(`^(?:http(s)?:\/\/)?(?:www\.)?(\w+[\w-]+\w+\.)?gitlab\.com[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]{3,100}$`) + valid := re.MatchString(strings.TrimSpace(gitlabOrg)) + if !valid { + return fmt.Sprintf("invalid Gitlab organization: %s", gitlabOrg), false + } + + return "", true +} + +// IsUUIDv4 returns true if the specified ID is in the UUIDv4 format, otherwise returns false +func IsUUIDv4(id string) bool { + value, err := uuid.FromString(id) + if err != nil { + return false + } + + return value.Version() == uuid.V4 +} + +// IsSalesForceID returns true if the specified ID is a SalesForce formatted ID, otherwise returns false +func IsSalesForceID(id string) bool { + regExp := regexp.MustCompile("^[a-zA-Z0-9]{18}|[a-zA-Z0-9]{15}$") + return regExp.MatchString(strings.TrimSpace(id)) +} diff --git a/cla-backend-go/v2/acs-service/client.go b/cla-backend-go/v2/acs-service/client.go index 337efc62c..3cb01c9b5 100644 --- a/cla-backend-go/v2/acs-service/client.go +++ b/cla-backend-go/v2/acs-service/client.go @@ -67,19 +67,36 @@ func GetClient() *Client { return acsServiceClient } +// SendUserInviteInput input model for sending user invites +type SendUserInviteInput struct { + InviteUserFirstName string + InviteUserLastName string + InviteUserEmail string + RoleName string + Scope string + ProjectSFID string + OrganizationSFID string + InviteType string + Subject string + EmailContent string + Automate bool +} + // SendUserInvite invites users to the LFX platform -func (ac *Client) SendUserInvite(ctx context.Context, email *string, - roleName string, scope string, projectID *string, organizationID string, inviteType string, subject *string, emailContent *string, automate bool) error { +// func (ac *Client) SendUserInvite(ctx context.Context, email *string, +// +// roleName string, scope string, projectID *string, organizationID string, inviteType string, subject *string, emailContent *string, automate bool) error { +func (ac *Client) SendUserInvite(ctx context.Context, input *SendUserInviteInput) error { f := logrus.Fields{ - "functionName": "SendUserInvite", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "roleName": roleName, - "scope": scope, - "projectID": utils.StringValue(projectID), - "organizationID": organizationID, - "inviteType": inviteType, - "subject": utils.StringValue(subject), - "automate": automate, + "functionName": "SendUserInvite", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "roleName": input.RoleName, + "scope": input.Scope, + "projectSFID": input.ProjectSFID, + "organizationSFID": input.OrganizationSFID, + "inviteType": input.InviteType, + "subject": input.Subject, + "automate": input.Automate, } tok, err := token.GetToken() @@ -91,30 +108,32 @@ func (ac *Client) SendUserInvite(ctx context.Context, email *string, clientAuth := runtimeClient.BearerToken(tok) params := &invite.CreateUserInviteParams{ SendInvite: &models.CreateInvite{ - Automate: automate, - Email: email, - Scope: scope, - RoleName: roleName, - Type: inviteType, + Automate: input.Automate, + Email: &input.InviteUserEmail, + FirstName: input.InviteUserFirstName, + LastName: input.InviteUserLastName, + RoleName: input.RoleName, + Scope: input.Scope, + Type: input.InviteType, }, Context: ctx, } - if scope == utils.ProjectOrgScope && projectID == nil { + if input.Scope == utils.ProjectOrgScope && input.ProjectSFID == "" { log.WithFields(f).Warnf("Project ID required for project|organization scope, error: %+v", ErrProjectIDMissing) return ErrProjectIDMissing } - if scope == utils.ProjectOrgScope { + if input.Scope == utils.ProjectOrgScope { // Set project|organization scope - params.SendInvite.ScopeID = fmt.Sprintf("%s|%s", *projectID, organizationID) + params.SendInvite.ScopeID = fmt.Sprintf("%s|%s", input.ProjectSFID, input.OrganizationSFID) } else { - params.SendInvite.ScopeID = organizationID + params.SendInvite.ScopeID = input.OrganizationSFID } - if subject != nil { - params.SendInvite.Subject = *subject + if input.Subject != "" { + params.SendInvite.Subject = input.Subject } // Pass emailContent if passed in the args - if emailContent != nil { - params.SendInvite.Body = *emailContent + if input.EmailContent != "" { + params.SendInvite.Body = input.EmailContent } log.WithFields(f).Debugf("Submitting ACS Service CreateUserInvite with payload: %+v", params) @@ -367,7 +386,7 @@ func (ac *Client) RemoveCLAUserRolesByProjectOrganization(projectSFID, organizat return nil } -//UserScope entity representative of project and org for given role scope +// UserScope entity representative of project and org for given role scope type UserScope struct { Username string Email string diff --git a/cla-backend-go/v2/approvals/models.go b/cla-backend-go/v2/approvals/models.go new file mode 100644 index 000000000..74ea117c9 --- /dev/null +++ b/cla-backend-go/v2/approvals/models.go @@ -0,0 +1,20 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package approvals + +type ApprovalItem struct { + ApprovalID string `dynamodbav:"approval_id"` + SignatureID string `dynamodbav:"signature_id"` + DateAdded string `dynamodbav:"date_added"` + DateRemoved string `dynamodbav:"date_removed"` + DateCreated string `dynamodbav:"date_created"` + DateModified string `dynamodbav:"date_modified"` + ApprovalName string `dynamodbav:"approval_name"` + ApprovalCriteria string `dynamodbav:"approval_criteria"` + CompanyID string `dynamodbav:"company_id"` + ProjectID string `dynamodbav:"project_id"` + ApprovalCompanyName string `dynamodbav:"approval_company_name"` + Note string `dynamodbav:"note"` + Active bool `dynamodbav:"active"` +} diff --git a/cla-backend-go/v2/approvals/repository.go b/cla-backend-go/v2/approvals/repository.go new file mode 100644 index 000000000..2125b872d --- /dev/null +++ b/cla-backend-go/v2/approvals/repository.go @@ -0,0 +1,609 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package approvals + +import ( + "errors" + "fmt" + + // "math/rand" + "net" + "sync" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "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" + "github.com/sirupsen/logrus" +) + +type IRepository interface { + GetApprovalList(approvalID string) (*ApprovalItem, error) + DeleteAll() error + GetApprovalListBySignature(signatureID string) ([]ApprovalItem, error) + AddApprovalList(approvalItem ApprovalItem) error + UpdateApprovalItem(approvalItem ApprovalItem) error + DeleteApprovalList(approvalID string) error + SearchApprovalList(criteria, approvalListName, claGroupID, companyID, signatureID string) ([]ApprovalItem, error) + BatchAddApprovalList(approvalItems []ApprovalItem) error + BatchDeleteApprovalList() error +} + +type repository struct { + stage string + dynamoDBClient *dynamodb.DynamoDB + tableName string +} + +func NewRepository(stage string, awsSession *session.Session, tableName string) IRepository { + return &repository{ + stage: stage, + dynamoDBClient: dynamodb.New(awsSession), + tableName: tableName, + } +} + +func (repo *repository) getAll() ([]*ApprovalItem, error) { + f := logrus.Fields{ + "functionName": "getAll", + } + + log.WithFields(f).Debugf("repository.getAll - fetching all approval lists") + + // Get all the records + pageSize := int64(100) + + scanInput := &dynamodb.ScanInput{ + TableName: aws.String(repo.tableName), + Limit: aws.Int64(pageSize), + } + + var results []*ApprovalItem + for { + result, err := repo.dynamoDBClient.Scan(scanInput) + if err != nil { + log.WithFields(f).Warnf("repository.getAll - unable to scan table, error: %+v", err) + return nil, err + } + + var items []*ApprovalItem + err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &items) + if err != nil { + log.WithFields(f).Warnf("repository.getAll - unable to unmarshal data from table, error: %+v", err) + return nil, err + } + + results = append(results, items...) + + if result.LastEvaluatedKey == nil { + break + } + + scanInput.ExclusiveStartKey = result.LastEvaluatedKey + } + + return results, nil +} + +func (repo *repository) DeleteAll() error { + f := logrus.Fields{ + "functionName": "DeleteAll", + } + + log.WithFields(f).Debugf("repository.DeleteAll - deleting all approval lists") + itemsToDelete, err := repo.getAll() + + if err != nil { + log.WithFields(f).Warnf("repository.DeleteAll - unable to fetch data from table, error: %+v", err) + return err + } + + log.WithFields(f).Debugf("repository.DeleteAll - deleting %d approval list items", len(itemsToDelete)) + + // Delete all the records + for _, item := range itemsToDelete { + retry := 0 + for { + log.WithFields(f).Debugf("repository.DeleteAll - deleting approval list item: %+v", item) + deleteRequest := &dynamodb.DeleteItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "approval_id": { + S: aws.String(item.ApprovalID), + }, + }, + TableName: aws.String(repo.tableName), + } + + _, err = repo.dynamoDBClient.DeleteItem(deleteRequest) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == dynamodb.ErrCodeProvisionedThroughputExceededException { + if retry > 5 { + return fmt.Errorf("unable to delete approval list item - retry limit reached, error: %+v", err) + } + retry++ + continue + } + return fmt.Errorf("unable to delete approval list item, error: %+v", err) + } + break + } + } + return nil +} + +func (repo *repository) UpdateApprovalItem(approvalItem ApprovalItem) error { + f := logrus.Fields{ + "functionName": "v2.approvals.repository.UpdateApprovalItem", + "approvalID": approvalItem.ApprovalID, + } + + log.WithFields(f).Debugf("updating approval item: %+v", approvalItem) + + av, err := dynamodbattribute.MarshalMap(approvalItem) + if err != nil { + log.WithFields(f).Warnf("unable to marshal data, error: %+v", err) + return err + } + + _, err = repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + TableName: aws.String(repo.tableName), + Item: av, + }) + + if err != nil { + log.WithFields(f).Warnf("repository.UpdateApprovalItem - unable to update data in table, error: %+v", err) + return err + } + + return nil +} + +func (repo *repository) GetApprovalList(approvalID string) (*ApprovalItem, error) { + f := logrus.Fields{ + "functionName": "GetApprovalList", + "approvalID": approvalID, + } + + log.WithFields(f).Debugf("repository.GetApprovalList - fetching approval list by approvalID: %s", approvalID) + + result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ + TableName: aws.String(repo.tableName), + Key: map[string]*dynamodb.AttributeValue{ + "approval_id": { + S: aws.String(approvalID), + }, + }, + }) + if err != nil { + log.WithFields(f).Warnf("repository.GetApprovalList - unable to read data from table, error: %+v", err) + return nil, err + } + + if len(result.Item) == 0 { + log.WithFields(f).Warnf("repository.GetApprovalList - no approval list found for approvalID: %s", approvalID) + return nil, errors.New("approval list not found") + } + + approvalItem := ApprovalItem{} + err = dynamodbattribute.UnmarshalMap(result.Item, &approvalItem) + if err != nil { + log.WithFields(f).Warnf("repository.GetApprovalList - unable to unmarshal data from table, error: %+v", err) + return nil, err + } + + return &approvalItem, nil +} + +func exponentialBackoff(attempt int) time.Duration { + const maxBackoff = 30 * time.Second + backoff := time.Duration(1< maxBackoff { + backoff = maxBackoff + } + return backoff +} + +func (repo *repository) GetApprovalListBySignature(signatureID string) ([]ApprovalItem, error) { + f := logrus.Fields{ + "functionName": "GetApprovalListBySignature", + "signatureID": signatureID, + } + + log.WithFields(f).Debugf("repository.GetApprovalListBySignature - fetching approval list by signatureID: %s", signatureID) + + condition := expression.Key("signature_id").Equal(expression.Value(signatureID)) + + expr, err := expression.NewBuilder().WithKeyCondition(condition).Build() + + if err != nil { + return nil, err + } + + pageSize := int64(100) + + input := &dynamodb.QueryInput{ + TableName: aws.String(repo.tableName), + IndexName: aws.String("signature-id-index"), + KeyConditionExpression: expr.KeyCondition(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + Limit: aws.Int64(pageSize), + } + + var results []ApprovalItem + maxRetries := 5 + + for attempt := 0; attempt < maxRetries; attempt++ { + output, err := repo.dynamoDBClient.Query(input) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + log.WithFields(f).Warnf("error building expression, error: %+v", err) + } else if awsErr, ok := err.(awserr.Error); ok { + log.WithFields(f).Warnf("error building expression, error: %+v", awsErr) + } else if err.Error() == "connection reset by peer" { + log.WithFields(f).Warnf("error building expression, error: %+v", err) + } else { + return nil, err + } + + time.Sleep(exponentialBackoff(attempt)) + } + + var items []ApprovalItem + err = dynamodbattribute.UnmarshalListOfMaps(output.Items, &items) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling data, error: %+v", err) + return nil, err + } + + results = append(results, items...) + + if output.LastEvaluatedKey == nil { + break + } + + input.ExclusiveStartKey = output.LastEvaluatedKey + } + + return results, nil + +} + +func (repo *repository) BatchDeleteApprovalList() error { + f := logrus.Fields{ + "functionName": "v2.BatchDeleteApprovalList", + "tableName": repo.tableName, + } + + log.WithFields(f).Debugf("repository.BatchDeleteApprovalList - deleting all approval list items") + + itemsToDelete, err := repo.getAll() + startTime := time.Now() + + if err != nil { + log.WithFields(f).Warnf("repository.BatchDeleteApprovalList - unable to fetch data from table, error: %+v", err) + return err + } + + log.WithFields(f).Debugf("repository.BatchDeleteApprovalList - deleting %d approval list items", len(itemsToDelete)) + + batchSize := 25 + deleted := len(itemsToDelete) + processed := 0 + var wg sync.WaitGroup + + for num := 0; num < len(itemsToDelete); num += batchSize { + start := num + end := num + batchSize + if end > len(itemsToDelete) { + end = len(itemsToDelete) + } + + wg.Add(1) + + go func(s, e int) { + defer wg.Done() + var batchWriteItems []*dynamodb.WriteRequest + for _, approvalItem := range itemsToDelete[s:e] { + batchWriteItems = append(batchWriteItems, &dynamodb.WriteRequest{ + DeleteRequest: &dynamodb.DeleteRequest{ + Key: map[string]*dynamodb.AttributeValue{ + "approval_id": { + S: aws.String(approvalItem.ApprovalID), + }, + }, + }, + }) + } + + input := &dynamodb.BatchWriteItemInput{ + RequestItems: map[string][]*dynamodb.WriteRequest{ + repo.tableName: batchWriteItems, + }, + } + + maxAttempts := 3 + + for attempt := 0; attempt < maxAttempts; attempt++ { + op, err := repo.dynamoDBClient.BatchWriteItem(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == dynamodb.ErrCodeProvisionedThroughputExceededException { + log.WithFields(f).Warnf("repository.BatchDeleteApprovalList - provisioned throughput exceeded, retrying in :%d seconds", exponentialBackoff(attempt)) + time.Sleep(exponentialBackoff(attempt)) + continue + } + log.WithFields(f).Warnf("repository.BatchDeleteApprovalList - unable to batch delete data from table, error: %+v", err) + return + } + + if len(op.UnprocessedItems) != 0 { + log.WithFields(f).Warn("unprocessed items found") + deleted -= len(op.UnprocessedItems) + } + break + } + + processed += len(batchWriteItems) + + log.WithFields(f).Debugf("repository.BatchDeleteApprovalList - processed %d of %d approval list items", processed, len(itemsToDelete)) + + }(start, end) + + } + + log.WithFields(f).Debug("repository.BatchDeleteApprovalList - waiting for batch delete to complete") + wg.Wait() + + log.WithFields(f).Debugf("all batches completed: deleted %d records in %s", deleted, time.Since(startTime)) + + return nil + +} + +func (repo *repository) BatchAddApprovalList(approvalItems []ApprovalItem) error { + f := logrus.Fields{ + "functionName": "BatchAddApprovalList", + "tableName": repo.tableName, + } + + log.WithFields(f).Debugf("repository.BatchAddApprovalList - adding %d approval list items", len(approvalItems)) + batchSize := 25 + processed := 0 + inserted := len(approvalItems) + startTime := time.Now() + var wg sync.WaitGroup + + for num := 0; num < len(approvalItems); num += batchSize { + start := num + end := num + batchSize + if end > len(approvalItems) { + end = len(approvalItems) + } + + wg.Add(1) + + go func(s, e int) { + defer wg.Done() + var batchWriteItems []*dynamodb.WriteRequest + for _, approvalItem := range approvalItems[s:e] { + av, err := dynamodbattribute.MarshalMap(approvalItem) + if err != nil { + log.WithFields(f).Warnf("repository.BatchAddApprovalList - unable to marshal data, error: %+v", err) + return + } + + batchWriteItems = append(batchWriteItems, &dynamodb.WriteRequest{ + PutRequest: &dynamodb.PutRequest{ + Item: av, + }, + }) + } + + input := &dynamodb.BatchWriteItemInput{ + RequestItems: map[string][]*dynamodb.WriteRequest{ + repo.tableName: batchWriteItems, + }, + } + + maxAttempts := 3 + + for attempt := 0; attempt < maxAttempts; attempt++ { + op, err := repo.dynamoDBClient.BatchWriteItem(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == dynamodb.ErrCodeProvisionedThroughputExceededException { + log.WithFields(f).Warnf("repository.BatchAddApprovalList - provisioned throughput exceeded, retrying in :%d seconds", exponentialBackoff(attempt)) + time.Sleep(exponentialBackoff(attempt)) + continue + } + log.WithFields(f).Warnf("repository.BatchAddApprovalList - unable to batch add data to table, error: %+v", err) + return + } + + if len(op.UnprocessedItems) != 0 { + log.WithFields(f).Warn("unprocessed items found") + inserted -= len(op.UnprocessedItems) + } + break + } + processed += len(batchWriteItems) + log.WithFields(f).Debugf("repository.BatchAddApprovalList - processed %d of %d approval list items", processed, len(approvalItems)) + + }(start, end) + + } + + log.WithFields(f).Debug("repository.BatchAddApprovalList - waiting for batch add to complete") + wg.Wait() + + log.WithFields(f).Debugf("all batches completed: inserted %d records in %s", inserted, time.Since(startTime)) + + return nil +} + +func (repo *repository) AddApprovalList(approvalItem ApprovalItem) error { + f := logrus.Fields{ + "functionName": "v2.approvals.repository.AddApprovalList", + "approvalID": approvalItem.ApprovalID, + "approvalName": approvalItem.ApprovalName, + "tableName": repo.tableName, + } + + log.WithFields(f).Debugf("repository.AddApprovalList - adding approval list: %+v", approvalItem) + + av, err := dynamodbattribute.MarshalMap(approvalItem) + if err != nil { + log.WithFields(f).Warnf("repository.AddApprovalList - unable to marshal data, error: %+v", err) + return err + } + + const maxRetries = 5 + var retryDelay time.Duration = 1 + + for attempt := 0; attempt < maxRetries; attempt++ { + _, err = repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + TableName: aws.String(repo.tableName), + Item: av, + }) + + if err != nil { + netErr, ok := err.(net.Error) + if ok && netErr.Timeout() { + log.WithFields(f).Warnf("repository.AddApprovalList - timeout error, retrying in %d seconds", retryDelay) + time.Sleep(exponentialBackoff(attempt)) + continue + } + awsErr, ok := err.(awserr.Error) + if !ok { + log.WithFields(f).Warnf("repository.AddApprovalList - unable to add data to table, error: %+v", err) + return err + } + + switch awsErr.Code() { + case dynamodb.ErrCodeProvisionedThroughputExceededException: + log.WithFields(f).Warnf("repository.AddApprovalList - provisioned throughput exceeded, retrying in %d seconds", retryDelay) + time.Sleep(retryDelay * time.Second) + retryDelay = retryDelay * 2 // exponential backoff + continue + default: + log.WithFields(f).Warnf("repository.AddApprovalList - unable to add data to table, error: %+v", err) + return err + } + } + } + + return nil +} + +func (repo *repository) DeleteApprovalList(approvalID string) error { + f := logrus.Fields{ + "functionName": "DeleteApprovalList", + "approvalID": approvalID, + } + + log.WithFields(f).Debugf("repository.DeleteApprovalList - deleting approval list by approvalID: %s", approvalID) + + _, err := repo.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ + TableName: aws.String(repo.tableName), + Key: map[string]*dynamodb.AttributeValue{ + "approval_id": { + S: aws.String(approvalID), + }, + }, + }) + if err != nil { + log.WithFields(f).Warnf("repository.DeleteApprovalList - unable to delete data from table, error: %+v", err) + return err + } + + return nil +} + +func (repo *repository) SearchApprovalList(criteria, approvalListName, claGroupID, companyID, signatureID string) ([]ApprovalItem, error) { + f := logrus.Fields{ + "functionName": "approvals.repository.SearchApprovalList", + "criteria": criteria, + "approvalName": approvalListName, + "claGroupID": claGroupID, + "companyID": companyID, + "signatureID": signatureID, + } + + pageSize := int64(100) + + if signatureID == "" { + return nil, errors.New("signatureID is required") + } + if approvalListName == "" { + return nil, errors.New("approvalListName is required") + } + + condition := expression.Key("signature_id").Equal(expression.Value(signatureID)) + + log.WithFields(f).Debugf("searching for approval list by approvalName: %s", approvalListName) + filter := expression.Name("approval_name").Contains(approvalListName) + + if criteria != "" { + log.WithFields(f).Debugf("searching for criteria: %s", criteria) + filter = filter.And(expression.Name("approval_criteria").Contains(criteria)) + } + + if claGroupID != "" { + log.WithFields(f).Debugf("searching for claGroupID: %s", claGroupID) + filter = filter.And(expression.Name("project_id").Equal(expression.Value(claGroupID))) + } + + if companyID != "" { + log.WithFields(f).Debugf("searching for companyID: %s", companyID) + filter = filter.And(expression.Name("company_id").Equal(expression.Value(companyID))) + } + + expr, err := expression.NewBuilder().WithFilter(filter).WithKeyCondition(condition).Build() + + if err != nil { + log.WithFields(f).Warnf("error building expression, error: %+v", err) + return nil, err + } + + input := &dynamodb.QueryInput{ + TableName: aws.String(repo.tableName), + IndexName: aws.String("signature-id-index"), + KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + Limit: aws.Int64(pageSize), + } + + var results []ApprovalItem + + for { + output, err := repo.dynamoDBClient.Query(input) + if err != nil { + log.WithFields(f).Warnf("error retrieving approval list, error: %+v", err) + return nil, err + } + + var items []ApprovalItem + err = dynamodbattribute.UnmarshalListOfMaps(output.Items, &items) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling data, error: %+v", err) + return nil, err + } + + results = append(results, items...) + + if output.LastEvaluatedKey == nil { + break + } + + input.ExclusiveStartKey = output.LastEvaluatedKey + } + + return results, nil + +} diff --git a/cla-backend-go/v2/cla_groups/handlers.go b/cla-backend-go/v2/cla_groups/handlers.go index 226412854..2833ebad1 100644 --- a/cla-backend-go/v2/cla_groups/handlers.go +++ b/cla-backend-go/v2/cla_groups/handlers.go @@ -9,6 +9,9 @@ import ( "fmt" "strings" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + v1Project "github.com/communitybridge/easycla/cla-backend-go/project/service" + "github.com/aws/aws-sdk-go/aws" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" @@ -24,10 +27,10 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/cla_group" log "github.com/communitybridge/easycla/cla-backend-go/logging" - v1Project "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/utils" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" v2ProjectServiceClient "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/client/project" + v2ProjectServiceModels "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/models" "github.com/go-openapi/runtime/middleware" ) @@ -39,9 +42,13 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaGroupCreateClaGroupHandler", + "functionName": "v2.cla_groups.handlers.ClaGroupCreateClaGroupHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupName": aws.StringValue(params.ClaGroupInput.ClaGroupName), + "claGroupName": utils.StringValue(params.ClaGroupInput.ClaGroupName), + "foundationSFID": utils.StringValue(params.ClaGroupInput.FoundationSfid), + "cclaEnabled": utils.BoolValue(params.ClaGroupInput.CclaEnabled), + "iclaEnabled": utils.BoolValue(params.ClaGroupInput.IclaEnabled), + "cclaRequiresIcla": utils.BoolValue(params.ClaGroupInput.CclaRequiresIcla), "claGroupDescription": params.ClaGroupInput.ClaGroupDescription, "projectSFIDList": strings.Join(params.ClaGroupInput.ProjectSfidList, ","), "authUsername": params.XUSERNAME, @@ -49,15 +56,14 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P } // Check permissions - if !isUserHaveAccessToCLAProject(ctx, authUser, aws.StringValue(params.ClaGroupInput.FoundationSfid), projectClaGroupsRepo) { + if !isUserHaveAccessToCLAProject(ctx, authUser, utils.StringValue(params.ClaGroupInput.FoundationSfid), params.ClaGroupInput.ProjectSfidList, projectClaGroupsRepo) { msg := fmt.Sprintf("user %s does not have access to create a CLA Group with project scope of: %s", authUser.UserName, aws.StringValue(params.ClaGroupInput.FoundationSfid)) log.WithFields(f).Warn(msg) return cla_group.NewCreateClaGroupForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - claGroup, err := service.CreateCLAGroup(ctx, params.ClaGroupInput, utils.StringValue(params.XUSERNAME)) + claGroupSummary, err := service.CreateCLAGroup(ctx, authUser, params.ClaGroupInput, utils.StringValue(params.XUSERNAME)) if err != nil { - log.WithFields(f).WithError(err).Warn("unable to create the CLA Group") return cla_group.NewCreateClaGroupBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "400", Message: fmt.Sprintf("EasyCLA - 400 Bad Request - %s", err.Error()), @@ -65,15 +71,23 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P }) } + claGroupModel, err := service.GetCLAGroup(ctx, claGroupSummary.ClaGroupID) + if err != nil { + return cla_group.NewCreateClaGroupBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, "problem loading newly created CLA Group", err)) + } + // Log the event eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.CLAGroupCreated, - ProjectID: claGroup.ClaGroupID, - LfUsername: authUser.UserName, - EventData: &events.CLAGroupCreatedEventData{}, + EventType: events.CLAGroupCreated, + CLAGroupName: claGroupSummary.ClaGroupName, + CLAGroupID: claGroupSummary.ClaGroupID, + ClaGroupModel: claGroupModel, + ParentProjectSFID: claGroupSummary.FoundationSfid, + LfUsername: authUser.UserName, + EventData: &events.CLAGroupCreatedEventData{}, }) - return cla_group.NewCreateClaGroupOK().WithXRequestID(reqID).WithPayload(claGroup) + return cla_group.NewCreateClaGroupOK().WithXRequestID(reqID).WithPayload(claGroupSummary) }) api.ClaGroupUpdateClaGroupHandler = cla_group.UpdateClaGroupHandlerFunc(func(params cla_group.UpdateClaGroupParams, authUser *auth.User) middleware.Responder { @@ -81,7 +95,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaGroupUpdateClaGroupHandler", + "functionName": "v2.cla_groups.handlers.ClaGroupUpdateClaGroupHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, "authUsername": params.XUSERNAME, @@ -108,7 +122,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P return cla_group.NewUpdateClaGroupNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, "CLA Group not found", err)) } - if errors.Is(err, v1Project.ErrProjectDoesNotExist) { + if errors.Is(err, repository.ErrProjectDoesNotExist) { return cla_group.NewUpdateClaGroupNotFound().WithXRequestID(reqID).WithPayload( utils.ErrorResponseNotFoundWithError(reqID, "CLA Group not found", err)) } @@ -116,9 +130,32 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("unable to lookup CLA Group by ID: %s", params.ClaGroupID), err)) } + // check if there's any change at all + if params.Body.ClaGroupName == claGroupModel.ProjectName && params.Body.ClaGroupDescription == claGroupModel.ProjectDescription { + log.WithFields(f).Warn("no new values passed, nothing to change, aborting.") + return cla_group.NewUpdateClaGroupBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequest(reqID, "no new values passed, nothing to change, aborting.")) + } + + projectCLAGroupModels, projectCLAGroupErr := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, params.ClaGroupID) + if projectCLAGroupErr != nil { + msg := fmt.Sprintf("unable to load the Project to CLA Group mappings for CLA Group: %s - is this CLA Group configured?", params.ClaGroupID) + log.WithFields(f).Warn(msg) + return cla_group.NewUpdateClaGroupInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, projectCLAGroupErr)) + } + if len(projectCLAGroupModels) == 0 { + msg := fmt.Sprintf("unable to load the Project to CLA Group mappings for CLA Group: %s - is this CLA Group configured?", params.ClaGroupID) + log.WithFields(f).Warn(msg) + return cla_group.NewUpdateClaGroupInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerError(reqID, msg)) + } + var projectSFIDList []string + for _, model := range projectCLAGroupModels { + projectSFIDList = append(projectSFIDList, model.ProjectSFID) + } + // Check permissions - if !isUserHaveAccessToCLAProject(ctx, authUser, claGroupModel.FoundationSFID, projectClaGroupsRepo) { - msg := fmt.Sprintf("user %s does not have access to update an existing CLA Group with project scope of: %s", authUser.UserName, claGroupModel.FoundationSFID) + if !isUserHaveAccessToCLAProject(ctx, authUser, projectCLAGroupModels[0].FoundationSFID, projectSFIDList, projectClaGroupsRepo) { + msg := fmt.Sprintf("user %s does not have access to update an existing CLA Group with project scope of: %s or any of these: %s", authUser.UserName, projectCLAGroupModels[0].FoundationSFID, strings.Join(projectSFIDList, ",")) log.WithFields(f).Warn(msg) return cla_group.NewUpdateClaGroupForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } @@ -130,8 +167,17 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P utils.ErrorResponseBadRequest(reqID, fmt.Sprintf("unable to update the CLA Group Name or Description - values are the same for CLA Group ID: %s", params.ClaGroupID))) } - claGroup, err := service.UpdateCLAGroup(ctx, claGroupModel, params.Body, utils.StringValue(params.XUSERNAME)) + var oldCLAGroupName, oldCLAGroupDescription string + oldCLAGroupName = claGroupModel.ProjectName + oldCLAGroupDescription = claGroupModel.ProjectDescription + + claGroupSummary, err := service.UpdateCLAGroup(ctx, authUser, claGroupModel, params.Body) if err != nil { + // Return a 409 conflict if we have a duplicate name + if _, ok := err.(*utils.CLAGroupNameConflict); ok { + return cla_group.NewUpdateClaGroupConflict().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseConflictWithError(reqID, err.Error(), err)) + } log.WithFields(f).WithError(err).Warn("unable to update the CLA Group Name and/or Description - update failed") return cla_group.NewUpdateClaGroupBadRequest().WithXRequestID(reqID).WithPayload( utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("unable to update CLA Group by ID: %s", params.ClaGroupID), err)) @@ -139,16 +185,21 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P // Log the event eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.CLAGroupUpdated, - ProjectID: claGroup.ClaGroupID, - LfUsername: authUser.UserName, + EventType: events.CLAGroupUpdated, + ClaGroupModel: claGroupModel, + ProjectID: claGroupSummary.ClaGroupID, + ProjectSFID: projectCLAGroupModels[0].ProjectSFID, + ParentProjectSFID: projectCLAGroupModels[0].FoundationSFID, + LfUsername: authUser.UserName, EventData: &events.CLAGroupUpdatedEventData{ - ClaGroupName: params.Body.ClaGroupName, - ClaGroupDescription: params.Body.ClaGroupDescription, + NewClaGroupName: params.Body.ClaGroupName, + NewClaGroupDescription: params.Body.ClaGroupDescription, + OldClaGroupName: oldCLAGroupName, + OldClaGroupDescription: oldCLAGroupDescription, }, }) - return cla_group.NewUpdateClaGroupOK().WithXRequestID(reqID).WithPayload(claGroup) + return cla_group.NewUpdateClaGroupOK().WithXRequestID(reqID).WithPayload(claGroupSummary) }) api.ClaGroupDeleteClaGroupHandler = cla_group.DeleteClaGroupHandlerFunc(func(params cla_group.DeleteClaGroupParams, authUser *auth.User) middleware.Responder { @@ -156,7 +207,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaGroupDeleteClaGroupHandler", + "functionName": "v2.cla_groups.handlers.ClaGroupDeleteClaGroupHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": params.ClaGroupID, "authUsername": params.XUSERNAME, @@ -173,7 +224,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P XRequestID: reqID, }) } - if err == v1Project.ErrProjectDoesNotExist { + if err == repository.ErrProjectDoesNotExist { return cla_group.NewDeleteClaGroupNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "404", Message: fmt.Sprintf("EasyCLA - 404 Not Found - cla_group %s not found", @@ -190,7 +241,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P } // Check permissions - if !isUserHaveAccessToCLAProject(ctx, authUser, claGroupModel.FoundationSFID, projectClaGroupsRepo) { + if !isUserHaveAccessToCLAProject(ctx, authUser, claGroupModel.FoundationSFID, []string{claGroupModel.ProjectExternalID}, projectClaGroupsRepo) { msg := fmt.Sprintf("user %s does not have access to delete the CLA Group with project scope of: %s", authUser.UserName, claGroupModel.FoundationSFID) log.WithFields(f).Warn(msg) return cla_group.NewDeleteClaGroupForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) @@ -199,12 +250,8 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P err = service.DeleteCLAGroup(ctx, claGroupModel, authUser) if err != nil { log.WithFields(f).Warn(err) - return cla_group.NewDeleteClaGroupInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "500", - Message: fmt.Sprintf("EasyCLA - 500 Internal server error - error deleting CLA Group %s, error: %+v", - params.ClaGroupID, err), - XRequestID: reqID, - }) + return cla_group.NewDeleteClaGroupInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseInternalServerErrorWithError(reqID, fmt.Sprintf("error deleting CLA Group by ID: %s", params.ClaGroupID), err)) } eventsService.LogEvent(&events.LogEventArgs{ @@ -222,7 +269,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaGroupEnrollProjectsHandler", + "functionName": "v2.cla_groups.handlers.ClaGroupEnrollProjectsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "ClaGroupID": params.ClaGroupID, "authUsername": params.XUSERNAME, @@ -230,87 +277,101 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P "projectSFIDList": strings.Join(params.ProjectSFIDList, ","), } - cg, err := v1ProjectService.GetCLAGroupByID(ctx, params.ClaGroupID) - if err != nil { - if err, ok := err.(*utils.CLAGroupNotFound); ok { - return cla_group.NewEnrollProjectsNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("EasyCLA - 404 Not Found - %s", err.Error()), - XRequestID: reqID, - }) + claGroupModel, getCLAGroupErr := v1ProjectService.GetCLAGroupByID(ctx, params.ClaGroupID) + if getCLAGroupErr != nil { + if _, ok := getCLAGroupErr.(*utils.CLAGroupNotFound); ok { + return cla_group.NewEnrollProjectsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("problem loading CLA Group by ID: %s", params.ClaGroupID), getCLAGroupErr)) } - if err == v1Project.ErrProjectDoesNotExist { - return cla_group.NewEnrollProjectsNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("EasyCLA - 404 Not Found - cla_group %s not found", - params.ClaGroupID), - XRequestID: reqID, - }) + if getCLAGroupErr == repository.ErrProjectDoesNotExist { + return cla_group.NewEnrollProjectsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("problem loading CLA Group by ID: %s", params.ClaGroupID), getCLAGroupErr)) } - return cla_group.NewEnrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: fmt.Sprintf("EasyCLA - 500 Internal server error - error = %s", err.Error()), - XRequestID: reqID, - }) + return cla_group.NewEnrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseInternalServerErrorWithError(reqID, fmt.Sprintf("problem loading CLA Group by ID: %s", params.ClaGroupID), getCLAGroupErr)) } // Check permissions - if !isUserHaveAccessToCLAProject(ctx, authUser, cg.FoundationSFID, projectClaGroupsRepo) { - msg := fmt.Sprintf("user %s does not have access to enroll projects with project scope of: %s", authUser.UserName, cg.FoundationSFID) + if !isUserHaveAccessToCLAProject(ctx, authUser, claGroupModel.FoundationSFID, []string{claGroupModel.ProjectExternalID}, projectClaGroupsRepo) { + msg := fmt.Sprintf("user %s does not have access to enroll projects with project scope of: %s", authUser.UserName, claGroupModel.FoundationSFID) log.WithFields(f).Warn(msg) return cla_group.NewEnrollProjectsForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - if !cg.FoundationLevelCLA { - log.WithFields(f).Debug("locating project by sfid...") + if !claGroupModel.FoundationLevelCLA { + log.WithFields(f).Debug("cla group is not a foundation level CLA group - locating project by sfid...") psc := v2ProjectService.GetClient() for _, projectSFID := range params.ProjectSFIDList { project, projectErr := psc.GetProject(projectSFID) if projectErr != nil || project == nil { msg := fmt.Sprintf("Failed to get salesforce project: %s", projectSFID) - log.WithFields(f).Warn(msg) + log.WithFields(f).WithError(projectErr).Warn(msg) if _, ok := projectErr.(*v2ProjectServiceClient.GetProjectNotFound); ok { - return cla_group.NewEnrollProjectsNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("project not found with given ID. [%s]", projectSFID), - }) + return cla_group.NewEnrollProjectsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("project not found with ID: [%s]", projectSFID), projectErr)) + } + return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, msg, projectErr)) + } + var parentProject *v2ProjectServiceModels.ProjectOutputDetailed + // Handle the ONAP edge case + if utils.IsProjectHaveParent(project) { + parentProject, projectErr = psc.GetProject(utils.GetProjectParentSFID(project)) + if parentProject == nil || projectErr != nil { + msg := fmt.Sprintf("Failed to get parent: %s", utils.GetProjectParentSFID(project)) + log.WithFields(f).Warnf(msg) + return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } - return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, projectErr)) } - if project.ProjectType == utils.ProjectTypeProjectGroup { + if (utils.IsProjectHaveParent(project) && !utils.IsProjectCategory(project, parentProject)) || (utils.IsProjectHasRootParent(project) && project.ProjectType == utils.ProjectTypeProjectGroup) { msg := fmt.Sprintf("Unable to enroll salesforce foundation project: %s in project level cla-group.", projectSFID) return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } } + } + + claProjectSFIDs := make([]string, 0) + // Get the list of projects already enrolled in the CLA Group + claGroupProjects, getProjectsErr := projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, params.ClaGroupID) + if getProjectsErr != nil { + msg := fmt.Sprintf("problem loading projects for CLA Group by ID: %s", params.ClaGroupID) + log.WithFields(f).Warn(msg) } - err = service.EnrollProjectsInClaGroup(ctx, params.ClaGroupID, cg.FoundationSFID, params.ProjectSFIDList) - if err != nil { - if strings.Contains(err.Error(), "bad request") { - return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: fmt.Sprintf("EasyCLA - 400 Bad Request - %s", err.Error()), - XRequestID: reqID, - }) + if len(claGroupProjects) > 0 { + for _, claGroupProject := range claGroupProjects { + claProjectSFIDs = append(claProjectSFIDs, claGroupProject.ProjectSFID) } - return cla_group.NewEnrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "500", - Message: fmt.Sprintf("EasyCLA - 500 Internal server error - error = %s", err.Error()), - XRequestID: reqID, - }) } - eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.CLAGroupUpdated, - ClaGroupModel: cg, - LfUsername: authUser.UserName, - EventData: &events.CLAGroupUpdatedEventData{ - ClaGroupName: cg.ProjectName, - ClaGroupDescription: cg.ProjectDescription, - }, + // Enroll the project(s) into the CLA Group + enrollCLAGroupErr := service.EnrollProjectsInClaGroup(ctx, &EnrollProjectsModel{ + AuthUser: authUser, + CLAGroupID: params.ClaGroupID, + FoundationSFID: claGroupModel.FoundationSFID, + ProjectSFIDList: params.ProjectSFIDList, + ProjectLevel: !claGroupModel.FoundationLevelCLA, + CLAGroupProjects: claProjectSFIDs, }) + if enrollCLAGroupErr != nil { + if _, ok := enrollCLAGroupErr.(*utils.EnrollValidationError); ok { + return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, "unable to enroll projects in CLA Group", enrollCLAGroupErr)) + } + if _, ok := enrollCLAGroupErr.(*utils.EnrollError); ok { + return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, "unable to enroll projects in CLA Group", enrollCLAGroupErr)) + } + if strings.Contains(enrollCLAGroupErr.Error(), "bad request") { + return cla_group.NewEnrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, "unable to enroll projects in CLA Group", enrollCLAGroupErr)) + } + return cla_group.NewEnrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseInternalServerErrorWithError(reqID, "unable to enroll projects in CLA Group", enrollCLAGroupErr)) + } + return cla_group.NewEnrollProjectsOK().WithXRequestID(reqID) }) @@ -319,7 +380,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaGroupUnenrollProjectsHandler", + "functionName": "v2.cla_groups.handlers.ClaGroupUnenrollProjectsHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "ClaGroupID": params.ClaGroupID, "authUsername": params.XUSERNAME, @@ -327,62 +388,50 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P "projectSFIDList": strings.Join(params.ProjectSFIDList, ","), } - cg, err := v1ProjectService.GetCLAGroupByID(ctx, params.ClaGroupID) + claGroupModel, err := v1ProjectService.GetCLAGroupByID(ctx, params.ClaGroupID) if err != nil { if err, ok := err.(*utils.CLAGroupNotFound); ok { - return cla_group.NewUnenrollProjectsNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("EasyCLA - 404 Not Found - %s", err.Error()), - XRequestID: reqID, - }) + return cla_group.NewUnenrollProjectsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("unable to locate CLA Group by ID: %s", params.ClaGroupID), err)) } - if err == v1Project.ErrProjectDoesNotExist { - return cla_group.NewUnenrollProjectsNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("EasyCLA - 404 Not Found - cla_group %s not found", params.ClaGroupID), - XRequestID: reqID, - }) + if err == repository.ErrProjectDoesNotExist { + return cla_group.NewUnenrollProjectsNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("unable to locate CLA Group by ID: %s", params.ClaGroupID), err)) } - return cla_group.NewUnenrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: fmt.Sprintf("EasyCLA - 500 Internal server error - error = %s", err.Error()), - XRequestID: reqID, - }) + return cla_group.NewUnenrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("problem locating CLA Group by ID: %s", params.ClaGroupID), err)) } // Check permissions - if !isUserHaveAccessToCLAProject(ctx, authUser, cg.FoundationSFID, projectClaGroupsRepo) { - msg := fmt.Sprintf("user %s does not have access to unenroll projects with project scope of: %s", authUser.UserName, cg.FoundationSFID) + if !isUserHaveAccessToCLAProject(ctx, authUser, claGroupModel.FoundationSFID, []string{claGroupModel.ProjectExternalID}, projectClaGroupsRepo) { + msg := fmt.Sprintf("user %s does not have access to unenroll projects with project scope of: %s", authUser.UserName, claGroupModel.FoundationSFID) log.WithFields(f).Warn(msg) return cla_group.NewUnenrollProjectsForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - err = service.UnenrollProjectsInClaGroup(ctx, params.ClaGroupID, cg.FoundationSFID, params.ProjectSFIDList) + err = service.UnenrollProjectsInClaGroup(ctx, &UnenrollProjectsModel{ + AuthUser: authUser, + CLAGroupID: params.ClaGroupID, + FoundationSFID: claGroupModel.FoundationSFID, + ProjectSFIDList: params.ProjectSFIDList, + }) if err != nil { + if _, ok := err.(*utils.EnrollValidationError); ok { + return cla_group.NewUnenrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, "unable to enroll projects in CLA Group", err)) + } + if _, ok := err.(*utils.EnrollError); ok { + return cla_group.NewUnenrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, "unable to enroll projects in CLA Group", err)) + } if strings.Contains(err.Error(), "bad request") { - return cla_group.NewUnenrollProjectsBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "400", - Message: fmt.Sprintf("EasyCLA - 400 Bad Request - %s", err.Error()), - XRequestID: reqID, - }) + return cla_group.NewUnenrollProjectsBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("unable to unenroll projects for CLA Group ID: %s", params.ClaGroupID), err)) } - return cla_group.NewUnenrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "500", - Message: fmt.Sprintf("EasyCLA - 500 Internal server error - error = %s", err.Error()), - XRequestID: reqID, - }) + return cla_group.NewUnenrollProjectsInternalServerError().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseInternalServerErrorWithError(reqID, fmt.Sprintf("unable to unenroll projects for CLA Group ID: %s", params.ClaGroupID), err)) } - eventsService.LogEvent(&events.LogEventArgs{ - EventType: events.CLAGroupUpdated, - ClaGroupModel: cg, - LfUsername: authUser.UserName, - EventData: &events.CLAGroupUpdatedEventData{ - ClaGroupName: cg.ProjectName, - ClaGroupDescription: cg.ProjectDescription, - }, - }) - return cla_group.NewUnenrollProjectsOK().WithXRequestID(reqID) }) @@ -391,7 +440,7 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaGroupListClaGroupsUnderFoundationHandler", + "functionName": "v2.cla_groups.handlers.ClaGroupListClaGroupsUnderFoundationHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, "authUsername": params.XUSERNAME, @@ -405,27 +454,26 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P msg := fmt.Sprintf("Failed to get salesforce project: %s", params.ProjectSFID) log.WithFields(f).Warn(msg) if _, ok := projectErr.(*v2ProjectServiceClient.GetProjectNotFound); ok { - return cla_group.NewListClaGroupsUnderFoundationNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("project not found with given ID. [%s]", params.ProjectSFID), - }) + return cla_group.NewListClaGroupsUnderFoundationNotFound().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, fmt.Sprintf("project not found with ID: %s", params.ProjectSFID), projectErr)) } - return cla_group.NewListClaGroupsUnderFoundationBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, projectErr)) + return cla_group.NewListClaGroupsUnderFoundationBadRequest().WithXRequestID(reqID).WithPayload( + utils.ErrorResponseNotFoundWithError(reqID, msg, projectErr)) } log.WithFields(f).Debug("found project - evaluating parent...") var projectSFIDs []string // Add the foundation ID, if available - if project.Foundation != nil && project.Foundation.ID != "" { - log.WithFields(f).Debugf("parent project - found %s - adding to list of project IDs...", project.Foundation.ID) - projectSFIDs = append(projectSFIDs, project.Foundation.ID) + if utils.IsProjectHaveParent(project) { + log.WithFields(f).Debugf("parent project - found %s - adding to list of project IDs...", utils.GetProjectParentSFID(project)) + projectSFIDs = append(projectSFIDs, utils.GetProjectParentSFID(project)) } log.WithFields(f).Debug("project - adding to list of project IDs...") projectSFIDs = append(projectSFIDs, project.ID) // Check permissions log.WithFields(f).Debugf("checking permissions for %s", strings.Join(projectSFIDs, ",")) - if !utils.IsUserAuthorizedForAnyProjects(authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForAnyProjects(ctx, authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { msg := fmt.Sprintf("user %s does not have access to list projects with project scope of: %s", authUser.UserName, params.ProjectSFID) log.WithFields(f).Warn(msg) return cla_group.NewListClaGroupsUnderFoundationForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) @@ -498,22 +546,34 @@ func Configure(api *operations.EasyclaAPI, service Service, v1ProjectService v1P } // 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 +func isUserHaveAccessToCLAProject(ctx context.Context, authUser *auth.User, parentProjectSFID string, projectSFIDs []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, + "functionName": "v2.cla_groups.handlers.isUserHaveAccessToCLAProject", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "parentProjectSFID": parentProjectSFID, + "projectSFIDs": strings.Join(projectSFIDs, ","), + "userName": authUser.UserName, + "userEmail": authUser.Email, + } + + // Check the parent project SFID + log.WithFields(f).Debug("testing if user has access to the parent project SFID") + if utils.IsUserAuthorizedForProject(ctx, authUser, parentProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user has access to the parent project SFID: %s", parentProjectSFID) + return true } + log.WithFields(f).Debugf("user does not have access to the parent project SFID: %s", parentProjectSFID) - log.WithFields(f).Debug("testing if user has access to project SFID") - if utils.IsUserAuthorizedForProject(authUser, projectSFID, utils.ALLOW_ADMIN_SCOPE) { + // Check the project SFIDs + log.WithFields(f).Debug("testing if user has access to any of the provided project SFIDs") + if utils.IsUserAuthorizedForAnyProjects(ctx, authUser, projectSFIDs, utils.ALLOW_ADMIN_SCOPE) { + log.WithFields(f).Debugf("user has access at least one of the provided project SFIDs: %s", strings.Join(projectSFIDs, ",")) return true } + log.WithFields(f).Debugf("user does not have access any of the provided project SFID: %s", projectSFIDs) - log.WithFields(f).Debug("user doesn't have direct access to the projectSFID - loading CLA Group from project id...") - projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(projectSFID) + log.WithFields(f).Debug("user doesn't have direct access to the parentProjectSFID or the provided projects SFIDs - loading CLA Group from project id...") + projectCLAGroupModel, err := projectClaGroupsRepo.GetClaGroupIDForProject(ctx, parentProjectSFID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem loading project -> cla group mapping - returning false") return false @@ -525,11 +585,11 @@ func isUserHaveAccessToCLAProject(ctx context.Context, authUser *auth.User, proj 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) { + if utils.IsUserAuthorizedForProjectTree(ctx, 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) { + if utils.IsUserAuthorizedForProject(ctx, authUser, projectCLAGroupModel.FoundationSFID, utils.ALLOW_ADMIN_SCOPE) { log.WithFields(f).Debug("user has access to parent foundation...") return true } @@ -537,16 +597,16 @@ func isUserHaveAccessToCLAProject(ctx context.Context, authUser *auth.User, proj // 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) + 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 } - projectSFIDs := getProjectIDsFromModels(f, projectCLAGroupModel.FoundationSFID, projectCLAGroupModels) - f["projectIDs"] = strings.Join(projectSFIDs, ",") + mappedProjectSFIDs := getProjectIDsFromModels(f, projectCLAGroupModel.FoundationSFID, projectCLAGroupModels) + f["mappedProjectSFIDs"] = strings.Join(mappedProjectSFIDs, ",") 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, mappedProjectSFIDs, 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/cla_groups/helpers.go b/cla-backend-go/v2/cla_groups/helpers.go index 6bb1ad257..3bbf9a8a9 100644 --- a/cla-backend-go/v2/cla_groups/helpers.go +++ b/cla-backend-go/v2/cla_groups/helpers.go @@ -10,12 +10,18 @@ import ( "strings" "sync" + "github.com/communitybridge/easycla/cla-backend-go/config" + + "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" 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/utils" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" psproject "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/client/project" + v2ProjectServiceModels "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/models" "github.com/sirupsen/logrus" ) @@ -30,11 +36,12 @@ func (s *service) validateClaGroupInput(ctx context.Context, input *models.Creat if input.ClaGroupName == nil { return false, fmt.Errorf("missing CLA Group parameter") } + foundationSFID := *input.FoundationSfid claGroupName := *input.ClaGroupName f := logrus.Fields{ - "functionName": "validateClaGroupInput", + "functionName": "v2.cla_groups.helpers.validateClaGroupInput", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "ClaGroupName": claGroupName, "ClaGroupDescription": input.ClaGroupDescription, @@ -43,19 +50,37 @@ func (s *service) validateClaGroupInput(ctx context.Context, input *models.Creat "CclaEnabled": *input.CclaEnabled, "CclaRequiresIcla": *input.CclaRequiresIcla, "ProjectSfidList": strings.Join(input.ProjectSfidList, ","), + "templateID": input.TemplateFields.TemplateID, } log.WithFields(f).Debug("validating CLA Group input...") + + if input.TemplateFields.TemplateID == "" { + msg := "missing CLA Group template ID value" + log.WithFields(f).Warn(msg) + return false, errors.New(msg) + } + if !s.v1TemplateService.CLAGroupTemplateExists(ctx, input.TemplateFields.TemplateID) { + msg := "invalid template ID" + log.WithFields(f).Warn(msg) + return false, errors.New(msg) + } // First, check that all the required flags are set and make sense if foundationSFID == "" { - return false, fmt.Errorf("bad request: foundation_sfid cannot be empty") + msg := "bad request: foundation_sfid cannot be empty" + log.WithFields(f).Warn(msg) + return false, errors.New(msg) } if !*input.IclaEnabled && !*input.CclaEnabled { - return false, fmt.Errorf("bad request: can not create cla group with both icla and ccla disabled") + msg := "bad request: can not create cla group with both icla and ccla disabled" + log.WithFields(f).Warn(msg) + return false, errors.New(msg) } if *input.CclaRequiresIcla { if !(*input.IclaEnabled && *input.CclaEnabled) { - return false, fmt.Errorf("bad request: ccla_requires_icla can not be enabled if one of icla/ccla is disabled") + msg := "bad request: ccla_requires_icla can not be enabled if one of icla/ccla is disabled" + log.WithFields(f).Warn(msg) + return false, errors.New(msg) } } @@ -80,70 +105,21 @@ func (s *service) validateClaGroupInput(ctx context.Context, input *models.Creat return false, err } - // Look up any existing configuration with this foundation SFID in our database... - log.WithFields(f).Debug("loading existing project IDs by foundation SFID...") - claGroupProjectModels, lookupErr := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(foundationSFID) - if lookupErr != nil { - log.WithFields(f).Warnf("problem looking up foundation level CLA group using foundation ID: %s, error: %+v", foundationSFID, lookupErr) - return false, lookupErr - } - - // Do we have an existing Foundation Level CLA Group? We can't create a new CLA Group if we have an existing - // Foundation Level CLA Group setup - log.WithFields(f).Debug("checking for existing Foundation Level CLA Groups...") - for _, projectCLAGroupModel := range claGroupProjectModels { - // Do we have an existing Foundation Level setup? No need to check the input foundation SFID against this list - // since we did the query based on the foundation SFID. - if projectCLAGroupModel.FoundationSFID == projectCLAGroupModel.ProjectSFID { - msg := fmt.Sprintf("found existing foundation level CLA Group using foundation ID: %s - can't add new CLA Groups under this configuration", foundationSFID) - log.WithFields(f).Warn(msg) - return false, errors.New(msg) - } - } - log.WithFields(f).Debug("no existing Foundation Level CLA Groups found...") - - // Are we trying to create a Foundation Level CLA Group, but one or more of the sub-projects already in a CLA Group? - if isFoundationIDInList(foundationSFID, input.ProjectSfidList) { - log.WithFields(f).Debug("we have a Foundation Level CLA Group request - checking if any CLA Groups include sub-projects from the foundation...") - // Only do this comparison if we have a input foundation level CLA group situation... - exists, existingProjectIDs := anySubProjectsAlreadyConfigured(input.ProjectSfidList, claGroupProjectModels) - if exists { - // So, we have the situation where the input is a foundation level CLA (meaning this applies to all sub-projects) - // but we have at least one project that is currently in an existing CLA group (other than the one just provided) - // so....we don't allow this (we don't migrate or merge - just reject) - msg := fmt.Sprintf("found existing sub-project(s) under foundation ID: %s which are already associated with an existing CLA Group - unable to create a new foundationl level CLA Group - project IDs: %+v", foundationSFID, existingProjectIDs) - log.WithFields(f).Warn(msg) - return false, errors.New(msg) - } - - // Do we have any existing CLA Groups associated with this foundation? Since this is a Foundation Level CLA - // Group, we can't create it if we have existing CLA groups already in place for this foundation - if len(claGroupProjectModels) > 0 { - // Create a string array to hold the existing CLA details for the error message - var claGroupString []string - for _, claGroupProjectModel := range claGroupProjectModels { - claGroupString = append(claGroupString, fmt.Sprintf("%s - %s", claGroupProjectModel.ClaGroupName, claGroupProjectModel.ClaGroupID)) - } - msg := fmt.Sprintf("found existing CLA Groups under foundation ID: %s - unable to create a new foundationl level CLA Group - existing CLA Group(s): [%s]", - foundationSFID, strings.Join(claGroupString, ",")) - log.WithFields(f).Warn(msg) - return false, errors.New(msg) - } - } - log.WithFields(f).Debug("we have a Foundation Level CLA Group request - good, no CLA Groups include sub-projects from the foundation...") - // Is our parent the LF project? log.WithFields(f).Debugf("looking up LF parent project record...") - isLFParent, err := psc.IsTheLinuxFoundation(foundationProjectDetails.Parent) - if err != nil { - log.WithFields(f).Warnf("validation failure - unable to lookup %s project, error: %+v", utils.TheLinuxFoundation, err) - return false, err + isLFParent := false + if utils.IsProjectHaveParent(foundationProjectDetails) { + isLFParent, err = psc.IsTheLinuxFoundation(utils.GetProjectParentSFID(foundationProjectDetails)) + if err != nil { + log.WithFields(f).WithError(err).Warnf("validation failure - unable to lookup parent project by SFID: %s", utils.GetProjectParentSFID(foundationProjectDetails)) + return false, err + } } // If the foundation details in the platform project service indicates that this foundation has no parent or no // children/sub-project... (stand alone project situation) log.WithFields(f).Debug("checking to see if we have a standalone project...") - if (foundationProjectDetails.Parent == "" || isLFParent) && len(foundationProjectDetails.Projects) == 0 { + if isLFParent && len(foundationProjectDetails.Projects) == 0 { log.WithFields(f).Debug("we have a standalone project...") // Did the user actually pass in any projects? If none - add the foundation ID to the list and return to // indicate it is a "standalone project" @@ -166,86 +142,123 @@ func (s *service) validateClaGroupInput(ctx context.Context, input *models.Creat return false, fmt.Errorf("bad request: invalid project_sfid_list. This project does not have subprojects defined in SF but some are provided as input") } - // Any of the projects in an existing CLA Group? - log.WithFields(f).Debug("validating enrolled projects...") - err = s.validateEnrollProjectsInput(ctx, foundationSFID, input.ProjectSfidList) + projectLevelCLA := true + if isFoundationIDInList(*input.FoundationSfid, input.ProjectSfidList) { + projectLevelCLA = false + } + + err = s.validateEnrollProjectsInput(ctx, foundationSFID, input.ProjectSfidList, projectLevelCLA, []string{}) if err != nil { return false, err } return false, nil } -func (s *service) validateEnrollProjectsInput(ctx context.Context, foundationSFID string, projectSFIDList []string) error { +func (s *service) validateEnrollProjectsInput(ctx context.Context, foundationSFID string, projectSFIDList []string, projectLevel bool, claGroupProjects []string) error { //nolint f := logrus.Fields{ - "functionName": "validateEnrollProjectsInput", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "foundationSFID": foundationSFID, - "projectSFIDList": strings.Join(projectSFIDList, ","), + "functionName": "validateEnrollProjectsInput", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "foundationSFID": foundationSFID, + "projectSFIDList": strings.Join(projectSFIDList, ","), + "projectLevel": projectLevel, + "claGroupProjects": strings.Join(claGroupProjects, ","), } psc := v2ProjectService.GetClient() if len(projectSFIDList) == 0 { - log.WithFields(f).Warn("validation failure - there should be at least one subproject associated...") - return fmt.Errorf("bad request: there should be at least one subproject associated") + return errors.New("validation failure - there should be at least one project provided for the enroll request") } // fetch the foundation model details from the platform project service which includes a list of its sub projects foundationProjectDetails, err := psc.GetProject(foundationSFID) if err != nil { - log.WithFields(f).Warnf("validation failure - problem fetching project details from project service, error: %+v", err) + log.WithFields(f).WithError(err).Warnf("validation failure - problem fetching project details from project service for project: %s", foundationSFID) return err } + if foundationProjectDetails == nil { + return fmt.Errorf("validation failure - problem fetching project details for project: %s", foundationSFID) + } - // Is our parent the LF project? - log.WithFields(f).Debugf("looking up LF parent project record...") - isLFParent, err := psc.IsTheLinuxFoundation(foundationProjectDetails.Parent) + foundationProjectSummary, err := psc.GetSummary(ctx, foundationSFID) if err != nil { - log.WithFields(f).Warnf("validation failure - unable to lookup %s project, error: %+v", utils.TheLinuxFoundation, err) + log.WithFields(f).WithError(err).Warnf("validation failure - problem fetching project details for project: %s", foundationSFID) return err } + if foundationProjectSummary == nil { + return fmt.Errorf("validation failure - problem fetching project details from project service for project: %s", foundationSFID) + } + + // Combine all the projectSFID values and check to see if any are the project root - shouldn't be in the list + if psc.IsAnyProjectTheRootParent(append(projectSFIDList, foundationSFID)) { + return errors.New("validation failure - one of the input projects is the root Linux Foundation project") + } + + // build Tree that tracks parent and child projects + projectTree := buildProjectNode(foundationProjectSummary) + log.WithFields(f).Debugf("projectTree: %+v", projectTree) + + var invalidSiblingProjects []string + // Check to see if CLAGroup at ProjectLevel has no siblings + if projectLevel { + log.WithFields(f).Debugf("checking to see if CLAGroup at ProjectLevel has no siblings...") + for _, projectSFID := range projectSFIDList { + siblings := getSiblings(projectTree, projectSFID) + log.WithFields(f).Debugf("projectSFID: %s, siblings: %v", projectSFID, siblings) + if len(siblings) > 0 { + for _, claProject := range claGroupProjects { + for _, sibling := range siblings { + if sibling == claProject { + invalidSiblingProjects = append(invalidSiblingProjects, claProject) + } + } + } + } + } + } - // Let's check the foundation provided - does it have a parent? Only allowed parent is TLF - if foundationProjectDetails.Parent != "" && !isLFParent { - log.WithFields(f).Warnf("input validation failure - foundation_sfid of %s has a parent other than %s which is: %s", - foundationSFID, utils.TheLinuxFoundation, foundationProjectDetails.Parent) - return fmt.Errorf("bad request: input validation failure - foundation_sfid of %s has a parent other than %s which is: %s", - foundationSFID, utils.TheLinuxFoundation, foundationProjectDetails.Parent) + if len(invalidSiblingProjects) > 0 { + log.WithFields(f).Warnf("validation failure - one of the input projects is a sibling of the project level CLA Group: %s", strings.Join(invalidSiblingProjects, ",")) + return fmt.Errorf("validation failure - one of the input projects has siblings in the CLA Group: %s", strings.Join(invalidSiblingProjects, ",")) } - // Comment out the below as we want to support stand-alone projects - /* - if len(foundationProjectDetails.Projects) == 0 { - log.WithFields(f).Warn("validation failure - project does not have any subprojects") - return fmt.Errorf("bad request: invalid input to enroll projects. project does not have any subprojects") + // Is our parent the LF project? + log.WithFields(f).Debugf("looking up LF parent project record...") + isLFParent := false + // if we have a project tree parent ID - check to see if it is one of our root parents + if projectTree != nil && projectTree.Parent != nil && projectTree.Parent.ID != "" { + log.WithFields(f).Debug("checking if parent project is the Linux Foundation or LF Projects LLC...") + isLFParent, err = psc.IsTheLinuxFoundation(projectTree.Parent.ID) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("validation failure - unable to lookup %s ", utils.TheLinuxFoundation) + return err } - */ - // Check to see if all the provided enrolled projects are part of this foundation - foundationProjectIDList := utils.NewStringSet() - for _, pr := range foundationProjectDetails.Projects { - foundationProjectIDList.Add(pr.ID) + log.WithFields(f).Debugf("isLFParent: %t", isLFParent) } - invalidProjectSFIDs := utils.NewStringSet() + + // Make sure each project exists in the project service for _, projectSFID := range projectSFIDList { - // Ok to have foundation ID in the project list - this means it's a Foundation Level CLA Group - if foundationSFID == projectSFID { - continue + projectDetails, projErr := psc.GetProject(projectSFID) + if projErr != nil { + return fmt.Errorf("validation failure - unable to lookup project by ID %s due to the error: %+v", projectSFID, err) } - // If the input/provided project ID is not in the SF project list... - if !foundationProjectIDList.Include(projectSFID) { - invalidProjectSFIDs.Add(projectSFID) + if projectDetails == nil { + return fmt.Errorf("validation failure - unable to lookup project by ID %s", projectSFID) } + } - if invalidProjectSFIDs.Length() != 0 { - log.WithFields(f).Warnf("validation failure - provided projects are not under the SF foundation: %+v", invalidProjectSFIDs.List()) - return fmt.Errorf("bad request: invalid project_sfid: %+v. One or more provided projects are not under the SF foundation", invalidProjectSFIDs.List()) + // Check to see if all the provided enrolled projects are part of this foundation + if !allProjectsExistInTree(projectTree, projectSFIDList) { + log.WithFields(f).Warnf("validation failure - one or more provided projects are not under the project tree: %+v", projectTree.String()) + return fmt.Errorf("bad request: invalid project_sfid: %+v. One or more provided projects are not under the parent", projectTree.String()) } // check if projects are not already enabled - enabledProjects, err := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(foundationSFID) + enabledProjects, err := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(ctx, foundationSFID) if err != nil { return err } @@ -253,7 +266,8 @@ func (s *service) validateEnrollProjectsInput(ctx context.Context, foundationSFI for _, pr := range enabledProjects { enabledProjectList.Add(pr.ProjectSFID) } - invalidProjectSFIDs = utils.NewStringSet() + + invalidProjectSFIDs := utils.NewStringSet() for _, projectSFID := range projectSFIDList { // Ok to have foundation ID in the project list - no need to check if it's already in the sub-project enabled list if foundationSFID == projectSFID { @@ -283,69 +297,61 @@ func (s *service) validateUnenrollProjectsInput(ctx context.Context, foundationS psc := v2ProjectService.GetClient() if len(projectSFIDList) == 0 { - log.WithFields(f).Warn("validation failure - there should be at least one subproject associated...") - return fmt.Errorf("bad request: there should be at least one subproject associated") + return errors.New("validation failure - there should be at least one project provided for the unenroll request") } - // Comment out the below as we want to support project-level projects - /* log.WithFields(f).Debug("checking to see if foundation is in project list...") - if !isFoundationIDInList(foundationSFID, projectSFIDList) { - log.WithFields(f).Warn("validation failure - unable to unenroll Project Group from CLA Group") - return fmt.Errorf("bad request: unable to unenroll Project Group from CLA Group") - } */ // fetch the foundation model details from the platform project service which includes a list of its sub projects foundationProjectDetails, err := psc.GetProject(foundationSFID) if err != nil { - log.WithFields(f).Warnf("validation failure - problem fetching project details from project service, error: %+v", err) return err } + if foundationProjectDetails == nil { + return fmt.Errorf("validation failure - problem fetching project details for project: %s", foundationSFID) + } - // Is our parent the LF project? - log.WithFields(f).Debugf("looking up LF parent project record...") - isLFParent, err := psc.IsTheLinuxFoundation(foundationProjectDetails.Parent) + foundationProjectSummary, err := psc.GetSummary(ctx, foundationSFID) if err != nil { - log.WithFields(f).Warnf("validation failure - unable to lookup %s project, error: %+v", utils.TheLinuxFoundation, err) return err } + if foundationProjectSummary == nil { + return fmt.Errorf("validation failure - problem fetching project details for project: %s", foundationSFID) + } - if foundationProjectDetails.Parent != "" && !isLFParent { - log.WithFields(f).Warnf("input validation failure - foundation_sfid of %s has a parent other than %s which is: %s", - foundationSFID, utils.TheLinuxFoundation, foundationProjectDetails.Parent) - return fmt.Errorf("bad request: input validation failure - foundation_sfid of %s has a parent other than %s which is: %s", - foundationSFID, utils.TheLinuxFoundation, foundationProjectDetails.Parent) + // Combine all the projectSFID values and check to see if any are the project root - shouldn't be in the list + if psc.IsAnyProjectTheRootParent(append(projectSFIDList, foundationSFID)) { + return errors.New("validation failure - one of the input projects is the root Linux Foundation project") } - // Comment out the below as we want to support stand-alone projects - /* if len(foundationProjectDetails.Projects) == 0 { - log.WithFields(f).Warn("validation failure - project does not have any subprojects") - return fmt.Errorf("bad request: invalid input to enroll projects. project does not have any subprojects") - } */ + // Grab the existing list of project CLA Groups associated with this foundation + existingProjectCLAGroupModels, err := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(ctx, foundationSFID) + if err != nil { + return err + } + log.WithFields(f).Debugf("before unenroll, we have %d projects associated with the CLA Group - we will be removing %d and will have %d remaining.", len(existingProjectCLAGroupModels), len(projectSFIDList), len(existingProjectCLAGroupModels)-len(projectSFIDList)) - // Check to see if all the provided enrolled projects are part of this foundation - foundationProjectIDList := utils.NewStringSet() - for _, pr := range foundationProjectDetails.Projects { - foundationProjectIDList.Add(pr.ID) + if len(existingProjectCLAGroupModels)-len(projectSFIDList) < 0 { + return fmt.Errorf("validation failure - must have at least one project enrolled in the CLA group under parent: %s with ID: %s", foundationProjectDetails.Name, foundationSFID) } - invalidProjectSFIDs := utils.NewStringSet() - for _, projectSFID := range projectSFIDList { - // Ok to have foundation ID in the project list - this means it's a Foundation Level CLA Group - if foundationSFID == projectSFID { - continue - } - // If the input/provided project ID is not in the SF project list... - if !foundationProjectIDList.Include(projectSFID) { - invalidProjectSFIDs.Add(projectSFID) + // build Tree that tracks parent and child projects + projectTree := buildProjectNode(foundationProjectSummary) + + // Make sure each project exists in the project service + for _, projectSFID := range projectSFIDList { + _, projErr := psc.GetProject(projectSFID) + if projErr != nil { + return fmt.Errorf("validation failure - unable to lookup project by ID %s due to the error: %+v", projectSFID, err) } } - if invalidProjectSFIDs.Length() != 0 { - log.WithFields(f).Warnf("validation failure - provided projects are not under the SF foundation: %+v", invalidProjectSFIDs.List()) - return fmt.Errorf("bad request: invalid project_sfid: %+v. One or more of the provided projects are not under the SF foundation", invalidProjectSFIDs.List()) + // Check to see if all the provided enrolled projects are part of this foundation + if !allProjectsExistInTree(projectTree, projectSFIDList) { + log.WithFields(f).Warnf("validation failure - one or more provided projects are not under the project tree: %+v", projectTree.String()) + return fmt.Errorf("bad request: invalid project_sfid: %+v. One or more provided projects are not under the parent", projectTree.String()) } // check if projects are already enrolled/enabled - enabledProjects, err := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(foundationSFID) + enabledProjects, err := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(ctx, foundationSFID) if err != nil { return err } @@ -353,7 +359,7 @@ func (s *service) validateUnenrollProjectsInput(ctx context.Context, foundationS for _, pr := range enabledProjects { enabledProjectList.Add(pr.ProjectSFID) } - invalidProjectSFIDs = utils.NewStringSet() + invalidProjectSFIDs := utils.NewStringSet() for _, projectSFID := range projectSFIDList { // Ok to have foundation ID in the project list - no need to check if it's already in the sub-project enabled list if foundationSFID == projectSFID { @@ -374,36 +380,48 @@ func (s *service) validateUnenrollProjectsInput(ctx context.Context, foundationS return nil } -func (s *service) AssociateCLAGroupWithProjects(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error { +func (s *service) AssociateCLAGroupWithProjects(ctx context.Context, request *AssociateCLAGroupWithProjectsModel) error { f := logrus.Fields{ "functionName": "AssociateCLAGroupWithProjects", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "foundationSFID": foundationSFID, - "projectSFIDList": strings.Join(projectSFIDList, ","), + "authUserName": request.AuthUser.UserName, + "authUserEmail": request.AuthUser.Email, + "claGroupID": request.CLAGroupID, + "foundationSFID": request.FoundationSFID, + "projectSFIDList": strings.Join(request.ProjectSFIDList, ","), } // Associate the CLA Group with the project list in a go routine var errorList []error var wg sync.WaitGroup - wg.Add(len(projectSFIDList)) + wg.Add(len(request.ProjectSFIDList)) - for _, projectSFID := range projectSFIDList { + for _, projectSFID := range request.ProjectSFIDList { // Invoke the go routine - any errors will be handled below - go func(sfid string) { + go func(projectSFID, parentProjectSFID, claGroupID string) { defer wg.Done() - log.WithFields(f).Debugf("associating cla_group with project: %s", sfid) - err := s.projectsClaGroupsRepo.AssociateClaGroupWithProject(claGroupID, sfid, foundationSFID) + log.WithFields(f).Debugf("associating cla_group with project: %s", projectSFID) + err := s.projectsClaGroupsRepo.AssociateClaGroupWithProject(ctx, claGroupID, projectSFID, parentProjectSFID) if err != nil { - log.WithFields(f).WithError(err).Warnf("associating cla_group with project: %s failed", sfid) + log.WithFields(f).WithError(err).Warnf("associating cla_group with project: %s failed", projectSFID) log.WithFields(f).Debug("deleting stale entries from cla_group project association") - deleteErr := s.projectsClaGroupsRepo.RemoveProjectAssociatedWithClaGroup(claGroupID, projectSFIDList, false) + deleteErr := s.projectsClaGroupsRepo.RemoveProjectAssociatedWithClaGroup(ctx, claGroupID, request.ProjectSFIDList, false) if deleteErr != nil { log.WithFields(f).WithError(deleteErr).Warn("deleting stale entries from cla_group project association failed") } // Add the error to the error list errorList = append(errorList, err) } - }(projectSFID) + // add event log entry + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.CLAGroupEnrolledProject, + ProjectSFID: projectSFID, + ParentProjectSFID: parentProjectSFID, + CLAGroupID: claGroupID, + LfUsername: request.AuthUser.UserName, + EventData: &events.CLAGroupEnrolledProjectData{}, + }) + }(projectSFID, request.FoundationSFID, request.CLAGroupID) } // Wait for the go routines to finish @@ -412,37 +430,54 @@ func (s *service) AssociateCLAGroupWithProjects(ctx context.Context, claGroupID // If any errors while associating - return the first one if len(errorList) > 0 { - log.WithFields(f).WithError(errorList[0]).Warnf("encountered %d errors when associating %d projects with the CLA Group", len(errorList), len(projectSFIDList)) + log.WithFields(f).WithError(errorList[0]).Warnf("encountered %d errors when associating %d projects with the CLA Group", len(errorList), len(request.ProjectSFIDList)) return errorList[0] } return nil } -func (s *service) UnassociateCLAGroupWithProjects(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error { +func (s *service) UnassociateCLAGroupWithProjects(ctx context.Context, request *UnassociateCLAGroupWithProjectsModel) error { f := logrus.Fields{ "functionName": "UnassociateCLAGroupWithProjects", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "foundationSFID": foundationSFID, - "projectSFIDList": strings.Join(projectSFIDList, ","), + "authUserName": request.AuthUser.UserName, + "authUserEmail": request.AuthUser.Email, + "claGroupID": request.CLAGroupID, + "foundationSFID": request.FoundationSFID, + "projectSFIDList": strings.Join(request.ProjectSFIDList, ","), } - deleteErr := s.projectsClaGroupsRepo.RemoveProjectAssociatedWithClaGroup(claGroupID, projectSFIDList, false) + deleteErr := s.projectsClaGroupsRepo.RemoveProjectAssociatedWithClaGroup(ctx, request.CLAGroupID, request.ProjectSFIDList, false) if deleteErr != nil { log.WithFields(f).Warnf("problem disassociating projects with CLA Group, error: %+v", deleteErr) return deleteErr } + // If this is slow, we may want to run these in a go routine + for _, projectSFID := range request.ProjectSFIDList { + // add event log entry + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.CLAGroupUnenrolledProject, + ProjectSFID: projectSFID, + CLAGroupID: request.CLAGroupID, + LfUsername: request.AuthUser.UserName, + EventData: &events.CLAGroupUnenrolledProjectData{}, + }) + } + return nil } // EnableCLAService enable CLA service attribute in the project service for the specified project list -func (s *service) EnableCLAService(ctx context.Context, projectSFIDList []string) error { +func (s *service) EnableCLAService(ctx context.Context, authUser *auth.User, claGroupID string, projectSFIDList []string) error { f := logrus.Fields{ "functionName": "EnableCLAService", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, "projectSFIDList": strings.Join(projectSFIDList, ","), + "claGroupID": claGroupID, } log.WithFields(f).Debug("enabling CLA service in platform project service") @@ -454,17 +489,56 @@ func (s *service) EnableCLAService(ctx context.Context, projectSFIDList []string for _, projectSFID := range projectSFIDList { // Execute as a go routine - go func(psClient *v2ProjectService.Client, sfid string) { + go func(psClient *v2ProjectService.Client, claGroupID, projectSFID string) { defer wg.Done() - enableProjectErr := psClient.EnableCLA(sfid) + log.WithFields(f).Debugf("enabling project CLA service for project: %s...", projectSFID) + enableProjectErr := psClient.EnableCLA(ctx, projectSFID) if enableProjectErr != nil { - log.WithFields(f).WithError(enableProjectErr). - Warnf("unable to enable CLA service for project: %s, error: %+v", sfid, enableProjectErr) + log.WithFields(f).WithError(enableProjectErr).Warnf("unable to enable CLA service for project: %s, error: %+v", projectSFID, enableProjectErr) errorList = append(errorList, enableProjectErr) } else { - log.WithFields(f).Debugf("enabled CLA service for project: %s", sfid) + log.WithFields(f).Debugf("enabled CLA service for project: %s", projectSFID) + // add event log entry + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ProjectServiceCLAEnabled, + ProjectID: projectSFID, + CLAGroupID: claGroupID, + LfUsername: authUser.UserName, + EventData: &events.ProjectServiceCLAEnabledData{}, + }) + + // If we should enable the CLA Service for the parent + if config.GetConfig().EnableCLAServiceForParent { + log.WithFields(f).Debugf("enable parent project CLA service when child is enrolled flag is enabled") + parentProjectSFID, parentLookupErr := psc.GetParentProject(projectSFID) + if parentLookupErr != nil || parentProjectSFID == "" { + log.WithFields(f).WithError(parentLookupErr).Warnf("unable to lookup parent project SFID for project: %s", projectSFID) + } else { + isTheLF, lookupErr := psClient.IsTheLinuxFoundation(parentProjectSFID) + if lookupErr != nil || isTheLF { + log.WithFields(f).Debugf("skipping setting the enabled services on The Linux Foundation parent project(s) for parent project SFID: %s", parentProjectSFID) + } else { + log.WithFields(f).Debugf("enabling parent project CLA service for project SFID: %s...", parentProjectSFID) + enableProjectErr := psClient.EnableCLA(ctx, parentProjectSFID) + if enableProjectErr != nil { + log.WithFields(f).WithError(enableProjectErr).Warnf("unable to enable CLA service for project: %s, error: %+v", parentProjectSFID, enableProjectErr) + errorList = append(errorList, enableProjectErr) + } else { + log.WithFields(f).Debugf("enabled CLA service for parent project: %s", parentProjectSFID) + // add event log entry + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ProjectServiceCLAEnabled, + ProjectID: parentProjectSFID, + CLAGroupID: claGroupID, + LfUsername: authUser.UserName, + EventData: &events.ProjectServiceCLAEnabledData{}, + }) + } + } + } + } } - }(psc, projectSFID) + }(psc, claGroupID, projectSFID) } // Wait until all go routines are done wg.Wait() @@ -479,14 +553,16 @@ func (s *service) EnableCLAService(ctx context.Context, projectSFIDList []string } // DisableCLAService disable CLA service attribute in the project service for the specified project list -func (s *service) DisableCLAService(ctx context.Context, projectSFIDList []string) error { +func (s *service) DisableCLAService(ctx context.Context, authUser *auth.User, claGroupID string, projectSFIDList []string) error { f := logrus.Fields{ "functionName": "DisableCLAService", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, "projectSFIDList": strings.Join(projectSFIDList, ","), + "claGroupID": claGroupID, } - log.WithFields(f).Debug("disabling CLA service in platform project service") // Run this in parallel... var errorList []error var wg sync.WaitGroup @@ -495,17 +571,26 @@ func (s *service) DisableCLAService(ctx context.Context, projectSFIDList []strin for _, projectSFID := range projectSFIDList { // Execute as a go routine - go func(psClient *v2ProjectService.Client, sfid string) { + go func(psClient *v2ProjectService.Client, claGroupID, projectSFID string) { defer wg.Done() - disableProjectErr := psClient.DisableCLA(sfid) + log.WithFields(f).Debugf("disabling CLA service for project: %s", projectSFID) + disableProjectErr := psClient.DisableCLA(ctx, projectSFID) if disableProjectErr != nil { log.WithFields(f).WithError(disableProjectErr). - Warnf("unable to disable CLA service for project: %s, error: %+v", sfid, disableProjectErr) + Warnf("unable to disable CLA service for project: %s, error: %+v", projectSFID, disableProjectErr) errorList = append(errorList, disableProjectErr) } else { - log.WithFields(f).Debugf("disabled CLA service for project: %s", sfid) + log.WithFields(f).Debugf("disabled CLA service for project: %s", projectSFID) + // add event log entry + s.eventsService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ProjectServiceCLADisabled, + ProjectID: projectSFID, + CLAGroupID: claGroupID, + LfUsername: authUser.UserName, + EventData: &events.ProjectServiceCLADisabledData{}, + }) } - }(psc, projectSFID) + }(psc, claGroupID, projectSFID) } // Wait until all go routines are done wg.Wait() @@ -519,28 +604,6 @@ func (s *service) DisableCLAService(ctx context.Context, projectSFIDList []strin return nil } -func anySubProjectsAlreadyConfigured(inputProjectIDs []string, existingProjectIDs []*projects_cla_groups.ProjectClaGroup) (bool, []string) { - // Build a quick map of the existing project IDs on file... - set := make(map[string]struct{}) - var exists = struct{}{} - for _, existingProject := range existingProjectIDs { - set[existingProject.ProjectSFID] = exists - } - - // Look through the input project ID list - if any matches set the flag and add to the response list - var foundIDs []string - var response = false - - for _, id := range inputProjectIDs { - if _, ok := set[id]; ok { - response = true - foundIDs = append(foundIDs, id) - } - } - - return response, foundIDs -} - func toFoundationMapping(list []*projects_cla_groups.ProjectClaGroup) *models.FoundationMappingList { out := &models.FoundationMappingList{List: make([]*models.FoundationMapping, 0)} foundationMap := make(map[string]*models.FoundationMapping) @@ -594,3 +657,217 @@ func isFoundationIDInList(foundationSFID string, projectsSFIDs []string) bool { } return false } + +// // getUniqueCLAGroupIDs logic to extract the unique +// func getUniqueCLAGroupIDs(projectCLAGroupMappings []*projects_cla_groups.ProjectClaGroup) []string { +// // to ensure we get the distinct count +// claGroupsMap := map[string]bool{} +// for _, projectCLAGroupModel := range projectCLAGroupMappings { +// // ensure that following goroutine gets a copy of projectSFID +// projectCLAGroupClaGroupID := projectCLAGroupModel.ClaGroupID +// // No need to re-process the same CLA group +// if _, ok := claGroupsMap[projectCLAGroupClaGroupID]; ok { +// continue +// } + +// // Add entry into our map - so we know not to re-process this CLA Group +// claGroupsMap[projectCLAGroupClaGroupID] = true +// } + +// keys := make([]string, 0, len(claGroupsMap)) +// for k := range claGroupsMap { +// keys = append(keys, k) +// } + +// return keys +// } + +func buildProjectNode(projectSummaryList []*v2ProjectServiceModels.ProjectSummary) *ProjectNode { + f := logrus.Fields{ + "functionName": "buildProjectNode", + } + root := &ProjectNode{ + ID: "", + Name: "", + Children: nil, + } + + parentSFID := "" + parentName := "" + for _, projectSummaryEntry := range projectSummaryList { + log.WithFields(f).Debugf("Processing project summary entry: %+v", *projectSummaryEntry) + // Get ParentProject + parentProjectModel, err := v2ProjectService.GetClient().GetParentProjectModel(projectSummaryEntry.ID) + + if parentSFID == "" && err == nil && parentProjectModel != nil { + // Update our root node + root.Parent = &ProjectNode{ + ID: parentProjectModel.ID, + Name: parentProjectModel.Name, + Children: []*ProjectNode{root}, + } + + // Save the parentSFID + parentSFID = parentProjectModel.ID + parentName = parentProjectModel.Name + } + + if parentSFID != "" && err == nil && parentProjectModel != nil && parentSFID != parentProjectModel.ID { + //We have different parents !!! + log.Warnf("current parent Name: %s ID: %s does not match other parent Name: %s, parent ID: %s", parentName, parentSFID, parentProjectModel.Name, parentProjectModel.ID) + } + + root.Children = append(root.Children, getLeafNodeFromProjectSFID(projectSummaryEntry.ID, parentName, parentSFID)) + } + + return root +} + +func getLeafNodeFromProjectSFID(projectSFID, parentName, parentSFID string) *ProjectNode { + f := logrus.Fields{ + "functionName": "getLeafNodeFromProjectSFID", + "projectSFID": projectSFID, + "parentName": parentName, + "parentSFID": parentSFID, + } + log.WithFields(f).Debugf("building leaf node from projectSFID: %s", projectSFID) + + // Get ParentProject + projectModel, err := v2ProjectService.GetClient().GetProject(projectSFID) + if err != nil { + return nil + } + + node := &ProjectNode{ + ID: projectModel.ID, + Name: projectModel.Name, + Parent: &ProjectNode{ + ID: parentSFID, + Name: parentName, + }, + } + + // For this node, collect the list of child nodes... + for _, childNode := range projectModel.Projects { + node.Children = append(node.Children, getLeafNodeFromProjectSFID(childNode.ID, projectModel.Name, projectModel.ID)) + } + + return node +} + +// findByID searches for given projectSFID recursively using DFS algorithm +func findByID(node *ProjectNode, projectSFID string) *ProjectNode { + if node == nil { + return nil + } + if node.ID == projectSFID { + return node + } + + for _, child := range node.Children { + foundNode := findByID(child, projectSFID) + if foundNode != nil { + return foundNode + } + } + + return nil +} + +// GetProjectDescendants returns all descendants of given projectSummary (salesforce) +func GetProjectDescendants(projectSummary []*v2ProjectServiceModels.ProjectSummary) []string { + + descendants := make([]string, 0) + for _, project := range projectSummary { + if len(project.Projects) > 0 { + descendants = append(descendants, project.ID) + for _, child := range project.Projects { + descendants = append(descendants, child.ID) + } + } + } + + return descendants +} + +// getSiblings returns all siblings of a given projectSFID +func getSiblings(root *ProjectNode, projectSFID string) []string { + f := logrus.Fields{ + "functionName": "v2.project.utils.getSiblings", + "projectSFID": projectSFID, + } + + log.WithFields(f).Debugf("getting siblings for projectSFID: %s", projectSFID) + + if root == nil { + return []string{} + } + + siblings := make([]string, 0) + + // stores nodes level wise + var queue ProjectStack + + // push root node + queue.Push(root) + + // traverse all levels + for !queue.IsEmpty() { + log.WithFields(f).Debugf("queue is not empty, processing next level") + tempNode := queue.Peek() + queue, _ = queue.Pop() + for _, child := range tempNode.Children { + if child.ID == projectSFID { + // add all children of tempNode to siblings aside from projectSFID + for _, sibling := range tempNode.Children { + if sibling.ID != projectSFID { + siblings = append(siblings, sibling.ID) + } + } + break + } + // push child node + queue.Push(child) + } + + } + log.WithFields(f).Debugf("returning siblings: %+v", siblings) + + return siblings +} + +// allProjectsExistInTree searches for given list of projects in foundation items +func allProjectsExistInTree(node *ProjectNode, projectSFIDs []string) bool { + for _, projectSFID := range projectSFIDs { + found := findByID(node, projectSFID) + if found == nil { + return false + } + } + return true +} + +func (n *ProjectNode) String() string { + if n == nil { + return "" + } + msg := fmt.Sprintf("projectSFID: '%s', projectName: '%s'\n", n.ID, n.Name) + + if n.Parent == nil { + msg = fmt.Sprintf("%s 'parentProjectSFID':'%s','parentProjectName':'%s'\n", msg, "", "") + } else { + msg = fmt.Sprintf("%s 'parentProjectSFID':'%s','parentProjectName':'%s'\n", msg, n.Parent.ID, n.Parent.Name) + } + + if len(n.Children) == 0 { + msg = fmt.Sprintf("%s children: no children\n", msg) + } else { + for _, child := range n.Children { + if child != nil { + msg = fmt.Sprintf("%s 'child': %s\n", msg, child.String()) + } + } + } + + return msg +} diff --git a/cla-backend-go/v2/cla_groups/models.go b/cla-backend-go/v2/cla_groups/models.go new file mode 100644 index 000000000..d2b5474a9 --- /dev/null +++ b/cla-backend-go/v2/cla_groups/models.go @@ -0,0 +1,73 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package cla_groups + +import ( + "github.com/LF-Engineering/lfx-kit/auth" +) + +// EnrollProjectsModel model to encapsulate the enroll projects request +type EnrollProjectsModel struct { + AuthUser *auth.User + CLAGroupID string + FoundationSFID string + ProjectSFIDList []string + ProjectLevel bool + CLAGroupProjects []string +} + +// UnenrollProjectsModel model to encapsulate the unenroll projects request +type UnenrollProjectsModel struct { + AuthUser *auth.User + CLAGroupID string + FoundationSFID string + ProjectSFIDList []string +} + +// AssociateCLAGroupWithProjectsModel to encapsulate the associate request +type AssociateCLAGroupWithProjectsModel struct { + AuthUser *auth.User + CLAGroupID string + FoundationSFID string + ProjectSFIDList []string +} + +// UnassociateCLAGroupWithProjectsModel to encapsulate the unassociate request +type UnassociateCLAGroupWithProjectsModel struct { + AuthUser *auth.User + CLAGroupID string + FoundationSFID string + ProjectSFIDList []string +} + +// ProjectNode representing nested projects +type ProjectNode struct { + Parent *ProjectNode + ID string + Name string + Children []*ProjectNode +} + +type ProjectStack []*ProjectNode + +func (s *ProjectStack) Push(v *ProjectNode) { + *s = append(*s, v) +} + +func (s *ProjectStack) Pop() (ProjectStack, *ProjectNode) { + l := len(*s) + return (*s)[:l-1], (*s)[l-1] +} + +func (s *ProjectStack) IsEmpty() bool { + return len(*s) == 0 +} + +func (s *ProjectStack) Peek() *ProjectNode { + return (*s)[len(*s)-1] +} + +func (s *ProjectStack) Size() int { + return len(*s) +} diff --git a/cla-backend-go/v2/cla_groups/service.go b/cla-backend-go/v2/cla_groups/service.go index d760f1e24..a486a2f51 100644 --- a/cla-backend-go/v2/cla_groups/service.go +++ b/cla-backend-go/v2/cla_groups/service.go @@ -10,10 +10,12 @@ import ( "sort" "strings" "sync" + "time" - "github.com/aws/aws-sdk-go/aws" + "github.com/communitybridge/easycla/cla-backend-go/project/common" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" - "golang.org/x/sync/errgroup" + "github.com/aws/aws-sdk-go/aws" "github.com/LF-Engineering/lfx-kit/auth" v1ClaManager "github.com/communitybridge/easycla/cla-backend-go/cla_manager" @@ -28,20 +30,20 @@ import ( "github.com/jinzhu/copier" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" - "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/signatures" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" - v1Project "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" v1Template "github.com/communitybridge/easycla/cla-backend-go/template" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + v2ProjectServiceModels "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/models" "github.com/sirupsen/logrus" ) type service struct { - v1ProjectService v1Project.Service - v1TemplateService v1Template.Service + v1ProjectService service2.Service + v1TemplateService v1Template.ServiceInterface projectsClaGroupsRepo projects_cla_groups.Repository claManagerRequests v1ClaManager.IService signatureService signatureService.SignatureService @@ -53,22 +55,23 @@ type service struct { // Service interface type Service interface { - CreateCLAGroup(ctx context.Context, input *models.CreateClaGroupInput, projectManagerLFID string) (*models.ClaGroupSummary, error) - UpdateCLAGroup(ctx context.Context, claGroupModel *v1Models.ClaGroup, input *models.UpdateClaGroupInput, projectManagerLFID string) (*models.ClaGroupSummary, error) + CreateCLAGroup(ctx context.Context, authUser *auth.User, input *models.CreateClaGroupInput, projectManagerLFID string) (*models.ClaGroupSummary, error) + GetCLAGroup(ctx context.Context, claGroupID string) (*v1Models.ClaGroup, error) + UpdateCLAGroup(ctx context.Context, authUser *auth.User, claGroupModel *v1Models.ClaGroup, input *models.UpdateClaGroupInput) (*models.ClaGroupSummary, error) ListClaGroupsForFoundationOrProject(ctx context.Context, foundationSFID string) (*models.ClaGroupListSummary, error) ListAllFoundationClaGroups(ctx context.Context, foundationID *string) (*models.FoundationMappingList, error) DeleteCLAGroup(ctx context.Context, claGroupModel *v1Models.ClaGroup, authUser *auth.User) error - EnrollProjectsInClaGroup(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error - UnenrollProjectsInClaGroup(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error - AssociateCLAGroupWithProjects(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error - UnassociateCLAGroupWithProjects(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error - EnableCLAService(ctx context.Context, projectSFIDList []string) error - DisableCLAService(ctx context.Context, projectSFIDList []string) error + EnrollProjectsInClaGroup(ctx context.Context, request *EnrollProjectsModel) error + UnenrollProjectsInClaGroup(ctx context.Context, request *UnenrollProjectsModel) error + AssociateCLAGroupWithProjects(ctx context.Context, request *AssociateCLAGroupWithProjectsModel) error + UnassociateCLAGroupWithProjects(ctx context.Context, request *UnassociateCLAGroupWithProjectsModel) error + EnableCLAService(ctx context.Context, authUser *auth.User, claGroupID string, projectSFIDList []string) error + DisableCLAService(ctx context.Context, authUser *auth.User, claGroupID string, projectSFIDList []string) error ValidateCLAGroup(ctx context.Context, input *models.ClaGroupValidationRequest) (bool, []string) } // NewService returns instance of CLA group service -func NewService(projectService v1Project.Service, templateService v1Template.Service, projectsClaGroupsRepo projects_cla_groups.Repository, claMangerRequests v1ClaManager.IService, signatureService signatureService.SignatureService, metricsRepo metrics.Repository, gerritService gerrits.Service, repositoriesService repositories.Service, eventsService events.Service) Service { +func NewService(projectService service2.Service, templateService v1Template.ServiceInterface, projectsClaGroupsRepo projects_cla_groups.Repository, claMangerRequests v1ClaManager.IService, signatureService signatureService.SignatureService, metricsRepo metrics.Repository, gerritService gerrits.Service, repositoriesService repositories.Service, eventsService events.Service) Service { return &service{ v1ProjectService: projectService, // aka cla_group service of v1 v1TemplateService: templateService, @@ -82,7 +85,8 @@ func NewService(projectService v1Project.Service, templateService v1Template.Ser } } -func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGroupInput, projectManagerLFID string) (*models.ClaGroupSummary, error) { +// CreateCLAGroup creates a new CLA group +func (s *service) CreateCLAGroup(ctx context.Context, authUser *auth.User, input *models.CreateClaGroupInput, projectManagerLFID string) (*models.ClaGroupSummary, error) { // Validate the input log.WithField("input", input).Debugf("validating create cla group input") if input.IclaEnabled == nil || @@ -94,8 +98,10 @@ func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGro } f := logrus.Fields{ - "functionName": "CreateCLAGroup", + "functionName": "v2.cla_groups.service.CreateCLAGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, "ClaGroupName": aws.StringValue(input.ClaGroupName), "ClaGroupDescription": input.ClaGroupDescription, "FoundationSfid": aws.StringValue(input.FoundationSfid), @@ -104,12 +110,13 @@ func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGro "CclaRequiresIcla": aws.BoolValue(input.CclaRequiresIcla), "ProjectSfidList": strings.Join(input.ProjectSfidList, ","), "projectManagerLFID": projectManagerLFID, + "claGroupTemplate": input.TemplateFields.TemplateID, } log.WithFields(f).Debug("validating CLA Group input") standaloneProject, err := s.validateClaGroupInput(ctx, input) if err != nil { - log.WithFields(f).Warnf("validation of create cla group input failed") + log.WithFields(f).WithError(err).Warnf("validation of CLA Group input failed") return nil, err } @@ -140,6 +147,7 @@ func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGro ProjectACL: []string{projectManagerLFID}, ProjectICLAEnabled: *input.IclaEnabled, ProjectName: *input.ClaGroupName, + ProjectTemplateID: input.TemplateFields.TemplateID, Version: "v2", }) if err != nil { @@ -173,20 +181,40 @@ func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGro } log.WithFields(f).Debug("cla_group_template attached", pdfUrls) + // If this is the project under a Parent Project/Foundation and this project is the only project in the list, then + // we need to set the foundation SFID to the current project SFID value + // This addresses the issue when we have a parent project and child project situation where the parent has a CLA + // Group and this is a request to create a new CLA Group for the child project - this is new logic that we added + // to support nested CLA groups where some child projects can inherit the CLA Group from the parent project while + // other child projects can have their own CLA Group. + // claAnchorProject := *input.FoundationSfid + // foundationCLAGroup, err := s.v1ProjectService.GetClaGroupByProjectSFID(ctx, claAnchorProject, false) + // if err != nil { + // log.WithFields(f).WithError(err).Warnf("unable to get CLA Group by project SFID: %s", claAnchorProject) + // } + // if len(input.ProjectSfidList) == 1 && foundationCLAGroup != nil { + // claAnchorProject = input.ProjectSfidList[0] + // } + // Associate the specified projects with our new CLA Group - err = s.EnrollProjectsInClaGroup(ctx, claGroup.ProjectID, *input.FoundationSfid, input.ProjectSfidList) - if err != nil { + enrollErr := s.EnrollProjectsInClaGroup(ctx, &EnrollProjectsModel{ + AuthUser: authUser, + CLAGroupID: claGroup.ProjectID, + FoundationSFID: *input.FoundationSfid, + ProjectSFIDList: input.ProjectSfidList, + }) + if enrollErr != nil { // Oops, roll back logic - log.WithFields(f).Debug("enroll projects in CLA Group failure - deleting created cla group") + log.WithFields(f).WithError(enrollErr).Debug("enroll projects in CLA Group failure - deleting created cla group") deleteErr := s.v1ProjectService.DeleteCLAGroup(ctx, claGroup.ProjectID) if deleteErr != nil { log.WithFields(f).Error("deleting created cla group failed - manual cleanup required.", deleteErr) } - return nil, err + return nil, enrollErr } // Build the response model - subProjectList, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(claGroup.ProjectID) + subProjectList, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, claGroup.ProjectID) if err != nil { return nil, err } @@ -213,6 +241,7 @@ func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGro ClaGroupDescription: claGroup.ProjectDescription, ClaGroupID: claGroup.ProjectID, ClaGroupName: claGroup.ProjectName, + TemplateID: claGroup.ProjectTemplateID, FoundationSfid: claGroup.FoundationSFID, FoundationName: foundationName, IclaEnabled: claGroup.ProjectICLAEnabled, @@ -221,11 +250,19 @@ func (s *service) CreateCLAGroup(ctx context.Context, input *models.CreateClaGro }, nil } -func (s *service) UpdateCLAGroup(ctx context.Context, claGroupModel *v1Models.ClaGroup, input *models.UpdateClaGroupInput, projectManagerLFID string) (*models.ClaGroupSummary, error) { +// GetCLAGroup returns the CLA group associated with the specified ID +func (s *service) GetCLAGroup(ctx context.Context, claGroupID string) (*v1Models.ClaGroup, error) { + return s.v1ProjectService.GetCLAGroupByID(ctx, claGroupID) +} + +// UpdateCLAGroup updates the specified CLA group with the input details +func (s *service) UpdateCLAGroup(ctx context.Context, authUser *auth.User, claGroupModel *v1Models.ClaGroup, input *models.UpdateClaGroupInput) (*models.ClaGroupSummary, error) { // Validate the input f := logrus.Fields{ - "functionName": "UpdateCLAGroup", + "functionName": "v2.cla_groups.service.UpdateCLAGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, "claGroupID": claGroupModel.ProjectID, "ClaGroupName": input.ClaGroupName, "ClaGroupDescription": input.ClaGroupDescription, @@ -266,6 +303,7 @@ func (s *service) UpdateCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl ProjectACL: claGroupModel.ProjectACL, ProjectICLAEnabled: claGroupModel.ProjectICLAEnabled, ProjectCCLAEnabled: claGroupModel.ProjectCCLAEnabled, + ProjectTemplateID: claGroupModel.ProjectTemplateID, ProjectCCLARequiresICLA: claGroupModel.ProjectCCLARequiresICLA, ProjectIndividualDocuments: claGroupModel.ProjectIndividualDocuments, ProjectCorporateDocuments: claGroupModel.ProjectCorporateDocuments, @@ -280,7 +318,7 @@ func (s *service) UpdateCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl } // Load the project IDs for this CLA Group - subProjectList, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(claGroupModel.ProjectID) + subProjectList, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, claGroupModel.ProjectID) if err != nil { log.WithFields(f).WithError(err).Warnf("problem getting project IDs for CLA Group") return nil, err @@ -340,13 +378,18 @@ func (s *service) UpdateCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl } // ListClaGroupsForFoundationOrProject returns the CLA Group list for the specified foundation ID -func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, projectOrFoundationSFID string) (*models.ClaGroupListSummary, error) { +func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, projectOrFoundationSFID string) (*models.ClaGroupListSummary, error) { // nolint f := logrus.Fields{ - "functionName": "ListClaGroupsForFoundationOrProject", + "functionName": "v2.cla_groups.service.ListClaGroupsForFoundationOrProject", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectOrFoundationSFID": projectOrFoundationSFID, } + // setup some timeout for the whole operation + var cancelFunc context.CancelFunc + ctx, cancelFunc = context.WithTimeout(ctx, time.Second*20) + defer cancelFunc() + // Our list of CLA Groups associated with this foundation (could be > 1) or project (only 1) var v1ClaGroups = new(v1Models.ClaGroups) // Our response model for this function @@ -365,113 +408,130 @@ func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, proje return nil, &utils.SFProjectNotFound{ProjectSFID: projectOrFoundationSFID} } - // Lookup the foundation name - need this if we were a project - need to lookup parent ID/Name - var foundationID = sfProjectModelDetails.ID - var foundationName = sfProjectModelDetails.Name - - // If it's a project... - if sfProjectModelDetails.ProjectType == utils.ProjectTypeProject { - // Since this is a project and not a foundation, we'll want to set he parent foundation ID and name (which is - // our parent in this case) - log.WithFields(f).Debug("found 'project' in platform project service.") - if sfProjectModelDetails.ProjectOutput.Foundation != nil { - foundationID = sfProjectModelDetails.ProjectOutput.Foundation.ID - foundationName = sfProjectModelDetails.ProjectOutput.Foundation.Name - log.WithFields(f).Debugf("using parent foundation ID: %s and name: %s", foundationID, foundationName) - } else { - // Project with no parent - must be a standalone - use our ID and Name as the foundation - foundationID = sfProjectModelDetails.ID - foundationName = sfProjectModelDetails.Name - log.WithFields(f).Debugf("no parent - using project as foundation ID: %s and name: %s", foundationID, foundationName) - } - - log.WithFields(f).Debug("locating CLA Group mapping...") - projectCLAGroup, lookupErr := s.projectsClaGroupsRepo.GetClaGroupIDForProject(projectOrFoundationSFID) - if lookupErr != nil { - log.WithFields(f).Warnf("problem locating CLA group by project id, error: %+v", lookupErr) - return nil, &utils.ProjectCLAGroupMappingNotFound{ProjectSFID: projectOrFoundationSFID, Err: lookupErr} + // Try and check if parent exists and projectType + var parentDetails *v2ProjectServiceModels.ProjectOutputDetailed + var parentDetailErr error + + // If we have a parent... + if utils.IsProjectHaveParent(sfProjectModelDetails) { + var parentSFID string + // Use utility function that considers TLF and LF Projects, LLC + parentSFID, parentDetailErr = v2ProjectService.GetClient().GetParentProject(projectOrFoundationSFID) + if parentDetailErr != nil { + return nil, parentDetailErr } - log.WithFields(f).Debugf("loading CLA Group by ID: %s", projectCLAGroup.ClaGroupID) - v1ClaGroupsByProject, claGroupLoadErr := s.v1ProjectService.GetCLAGroupByID(ctx, projectCLAGroup.ClaGroupID) - //v1ClaGroupsByProject, prjerr := s.v1ProjectService.GetClaGroupByProjectSFID(projectOrFoundationSFID, DontLoadDetails) - if claGroupLoadErr != nil { - log.WithFields(f).Warnf("problem loading CLA group by id, error: %+v", claGroupLoadErr) - return nil, &utils.CLAGroupNotFound{CLAGroupID: projectCLAGroup.ClaGroupID, Err: claGroupLoadErr} - } - - v1ClaGroups.Projects = append(v1ClaGroups.Projects, *v1ClaGroupsByProject) - - v1CLAGroupData, v1ClaGroupErr := s.v1ProjectService.GetClaGroupByProjectSFID(ctx, projectOrFoundationSFID, false) - if v1ClaGroupErr != nil { - log.WithFields(f).Warnf("problem locating CLA group by project id, error: %+v", v1ClaGroupErr) - return nil, &utils.CLAGroupNotFound{CLAGroupID: projectOrFoundationSFID, Err: v1ClaGroupErr} + // Get Parent + parentDetails, parentDetailErr = v2ProjectService.GetClient().GetProject(parentSFID) + if parentDetailErr != nil || parentDetails == nil { + return nil, parentDetailErr } + } - _, found := Find(v1ClaGroups.Projects, v1CLAGroupData.ProjectID) - if !found { - v1ClaGroups.Projects = append(v1ClaGroups.Projects, *v1CLAGroupData) - } + // // Lookup the foundation name - need this if we were a project - need to lookup parent ID/Name + // var foundationID = sfProjectModelDetails.ID + // var foundationName = sfProjectModelDetails.Name + var foundationID string + var foundationName string - } else if sfProjectModelDetails.ProjectType == utils.ProjectTypeProjectGroup { - log.WithFields(f).Debug("found 'project group' in platform project service. Locating CLA Groups for foundation...") - projectCLAGroups, lookupErr := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(projectOrFoundationSFID) - if lookupErr != nil { - log.WithFields(f).Warnf("problem locating CLA group by project id, error: %+v", lookupErr) - return nil, &utils.ProjectCLAGroupMappingNotFound{ProjectSFID: projectOrFoundationSFID, Err: lookupErr} + // If it's a project... + if sfProjectModelDetails.ProjectType == utils.ProjectTypeProjectGroup || sfProjectModelDetails.ProjectType == utils.ProjectTypeProject { + log.WithFields(f).Debugf("looking up CLA Groups for project or Foundation: %s with projectType: %s", projectOrFoundationSFID, sfProjectModelDetails.ProjectType) + var appendErr error + foundationID, foundationName, appendErr = s.appendCLAGroupsForProject(ctx, f, projectOrFoundationSFID, sfProjectModelDetails, v1ClaGroups) + if appendErr != nil { + return nil, appendErr } - log.WithFields(f).Debugf("discovered %d projects based on foundation SFID...", len(projectCLAGroups)) + } else { + msg := fmt.Sprintf("unsupported foundation/project SFID type: %s", sfProjectModelDetails.ProjectType) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } - claGroupsMap := map[string]bool{} - // Load these CLA Group records in parallel - var eg errgroup.Group - for _, projectCLAGroup := range projectCLAGroups { - // ensure that following goroutine gets a copy of projectSFID - projectCLAGroupClaGroupID := projectCLAGroup.ClaGroupID - // No need to re-process the same CLA group - if _, ok := claGroupsMap[projectCLAGroupClaGroupID]; ok { - continue - } + log.WithFields(f).Debugf("Building response model for %d CLA Groups", len(v1ClaGroups.Projects)) - // Add entry into our map - so we know not to re-process this CLA Group - claGroupsMap[projectCLAGroupClaGroupID] = true + claGroupIDList, err := s.buildClaGroupSummaryResponseModel(ctx, f, v1ClaGroups, foundationName, foundationID, responseModel) + if err != nil { + return nil, err + } - // Invoke the go routine - any errors will be handled below - eg.Go(func() error { - log.WithFields(f).Debugf("loading CLA Group by ID: %s", projectCLAGroupClaGroupID) - claGroupModel, claGroupLookupErr := s.v1ProjectService.GetCLAGroupByID(ctx, projectCLAGroupClaGroupID) - if claGroupLookupErr != nil { - log.WithFields(f).Warnf("problem locating project by id: %s, error: %+v", projectCLAGroupClaGroupID, claGroupLookupErr) - return &utils.SFProjectNotFound{ProjectSFID: projectCLAGroupClaGroupID, Err: claGroupLookupErr} - } + // One more pass to update the metrics - bulk lookup the metrics and update the response model + log.WithFields(f).Debugf("Loading metrics for %d CLA Groups...", len(claGroupIDList.List())) + s.loadMetrics(ctx, f, responseModel, claGroupIDList) - v1ClaGroups.Projects = append(v1ClaGroups.Projects, *claGroupModel) - return nil - }) + // Sort the response based on the Foundation and CLA group name + sort.Slice(responseModel.List, func(i, j int) bool { + switch strings.Compare(responseModel.List[i].FoundationName, responseModel.List[j].FoundationName) { + case -1: + return true + case 1: + return false } + return responseModel.List[i].ClaGroupName < responseModel.List[j].ClaGroupName + }) - // Wait for the go routines to finish - log.WithFields(f).Debug("waiting for CLA Groups to load...") - if loadErr := eg.Wait(); loadErr != nil { - return nil, loadErr - } + return responseModel, nil +} - v1CLAGroupsData, v1ClaGroupErr := s.v1ProjectService.GetClaGroupsByFoundationSFID(ctx, projectOrFoundationSFID, false) - if v1ClaGroupErr != nil { - log.WithFields(f).Warnf("problem locating CLA group by project id, error: %+v", v1ClaGroupErr) - return nil, &utils.CLAGroupNotFound{CLAGroupID: projectOrFoundationSFID, Err: v1ClaGroupErr} - } +func (s *service) loadMetrics(ctx context.Context, f logrus.Fields, responseModel *models.ClaGroupListSummary, claGroupIDList *utils.StringSet) { + type MetricsResult struct { + index int + iclaSignatureCount int64 + cclaSignatureCount int64 + Error error + } + metricsResultChannel := make(chan *MetricsResult, len(responseModel.List)) + + for idx, responseEntry := range responseModel.List { + go func(index int, responseEntry *models.ClaGroupSummary) { + log.WithFields(f).Debugf("loading project signature metrics for CLA Group (idx:%d): %s - %s", index, responseEntry.ClaGroupID, responseEntry.ClaGroupName) + iclaSignatureDetails, err := s.signatureService.GetProjectSignatures(ctx, + signatures.GetProjectSignaturesParams{ + Approved: utils.Bool(true), + ClaType: aws.String(utils.ClaTypeICLA), + ProjectID: responseEntry.ClaGroupID, + Signed: utils.Bool(true), + }, + ) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error while getting ICLA Signature using CLA Group ID %s Error: %v", responseEntry.ClaGroupID, err) + } - v1ClaGroups = v1CLAGroupsData + cclaSignatureDetails, err := s.signatureService.GetProjectSignatures(ctx, + signatures.GetProjectSignaturesParams{ + Approved: utils.Bool(true), + ProjectID: responseEntry.ClaGroupID, + ClaType: aws.String(utils.ClaTypeCCLA), + Signed: utils.Bool(true), + }, + ) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error while getting ICLA Signature using CLA Group ID %s Error: %v", responseEntry.ClaGroupID, err) + } - } else { - msg := fmt.Sprintf("unsupported foundation/project SFID type: %s", sfProjectModelDetails.ProjectType) - log.WithFields(f).Warn(msg) - return nil, errors.New(msg) + metricsResultChannel <- &MetricsResult{ + index: index, + iclaSignatureCount: iclaSignatureDetails.ResultCount, + cclaSignatureCount: cclaSignatureDetails.ResultCount, + Error: err, + } + }(idx, responseEntry) } - log.WithFields(f).Debugf("Building response model for %d CLA Groups", len(v1ClaGroups.Projects)) + log.WithFields(f).Debugf("Waiting for metrics responses for %d CLA Groups...", len(claGroupIDList.List())) + for range responseModel.List { + select { + case response := <-metricsResultChannel: + log.WithFields(f).Debugf("Signature Metrics: CCLA Signatures: %d, ICLA Signatures: %d", response.cclaSignatureCount, response.iclaSignatureCount) + responseModel.List[response.index].TotalSignatures = response.cclaSignatureCount + response.iclaSignatureCount + case <-ctx.Done(): + log.WithError(ctx.Err()).Warnf("waiting for metrics failed with timeout") + return + } + } +} +func (s *service) buildClaGroupSummaryResponseModel(ctx context.Context, f logrus.Fields, v1ClaGroups *v1Models.ClaGroups, foundationName string, foundationID string, responseModel *models.ClaGroupListSummary) (*utils.StringSet, error) { claGroupIDList := utils.NewStringSet() // Build the response model for each CLA Group... @@ -480,11 +540,11 @@ func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, proje // Keep a list of the CLA Group IDs - we'll use it later to do a batch look in the metrics claGroupIDList.Add(v1ClaGroup.ProjectID) - currentICLADoc, docErr := v1Project.GetCurrentDocument(context.Background(), v1ClaGroup.ProjectIndividualDocuments) + currentICLADoc, docErr := common.GetCurrentDocument(ctx, v1ClaGroup.ProjectIndividualDocuments) if docErr != nil { log.WithFields(f).WithError(docErr).Warn("problem determining current ICLA for this CLA Group") } - currentCCLADoc, docErr := v1Project.GetCurrentDocument(context.Background(), v1ClaGroup.ProjectCorporateDocuments) + currentCCLADoc, docErr := common.GetCurrentDocument(ctx, v1ClaGroup.ProjectCorporateDocuments) if docErr != nil { log.WithFields(f).WithError(docErr).Warn("problem determining current CCLA for this CLA Group") } @@ -502,6 +562,7 @@ func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, proje ClaGroupDescription: v1ClaGroup.ProjectDescription, ClaGroupID: v1ClaGroup.ProjectID, ClaGroupName: v1ClaGroup.ProjectName, + TemplateID: v1ClaGroup.ProjectTemplateID, FoundationSfid: v1ClaGroup.FoundationSFID, FoundationName: foundationName, IclaEnabled: v1ClaGroup.ProjectICLAEnabled, @@ -514,7 +575,7 @@ func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, proje } // How many SF projects are associated with this CLA Group? - cgprojects, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(v1ClaGroup.ProjectID) + cgprojects, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, v1ClaGroup.ProjectID) if err != nil { return nil, &utils.ProjectCLAGroupMappingNotFound{CLAGroupID: v1ClaGroup.ProjectID, Err: err} } @@ -545,44 +606,124 @@ func (s *service) ListClaGroupsForFoundationOrProject(ctx context.Context, proje // Add this CLA Group to our response model responseModel.List = append(responseModel.List, cg) } + return claGroupIDList, nil +} - // One more pass to update the metrics - bulk lookup the metrics and update the response model - log.WithFields(f).Debugf("Loading metrics for %d CLA Groups - updating response", len(claGroupIDList.List())) - var iclaSignatureCount, cclaSignatureCount int64 - for _, responseEntry := range responseModel.List { - log.Debugf("cla group entry logs %s", responseEntry.ClaGroupID) - iclaSignatureDetails, err := s.signatureService.GetProjectSignatures(ctx, signatures.GetProjectSignaturesParams{ProjectID: responseEntry.ClaGroupID, ClaType: aws.String(utils.ClaTypeICLA), SignatureType: aws.String(utils.SignatureTypeCLA)}) - if err != nil { - log.Warnf("error while getting ICLA Signature using clagroupID %s Error: %v", responseEntry.ClaGroupID, err) - } - iclaSignatureCount = iclaSignatureDetails.ResultCount +// func (s *service) collateFoundationCLAGroups(ctx context.Context, uniqueCLAGroupList []string) []v1Models.ClaGroup { +// var wg sync.WaitGroup +// wg.Add(len(uniqueCLAGroupList)) +// claGroups := make([]v1Models.ClaGroup, 0) +// for _, claGroupID := range uniqueCLAGroupList { +// go func(claGroupID string) { +// defer wg.Done() +// claGroupModel, claGroupErr := s.v1ProjectService.GetCLAGroupByID(ctx, claGroupID) +// if claGroupErr != nil { +// log.Warnf("skipping - error looking up CLA Group by ID: %s, error: %+v", claGroupID, claGroupErr) +// } else { +// claGroups = append(claGroups, *claGroupModel) +// } +// }(claGroupID) +// } +// wg.Wait() +// return claGroups +// } + +// func (s *service) appendCLAGroupsForFoundation(ctx context.Context, f logrus.Fields, projectOrFoundationSFID string, v1ClaGroups *v1Models.ClaGroups) error { +// log.WithFields(f).Debug("found 'project group' in platform project service. Locating CLA Groups for foundation...") +// projectCLAGroupMappings, lookupErr := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(ctx, projectOrFoundationSFID) +// if lookupErr != nil { +// log.WithFields(f).Warnf("problem locating CLA group by project id, error: %+v", lookupErr) +// return &utils.ProjectCLAGroupMappingNotFound{ProjectSFID: projectOrFoundationSFID, Err: lookupErr} +// } +// log.WithFields(f).Debugf("discovered %d projects based on foundation SFID...", len(projectCLAGroupMappings)) + +// // Determine how many CLA Groups we have - we could have many and possibly return duplicates, we use this loop +// uniqueCLAGroupList := getUniqueCLAGroupIDs(projectCLAGroupMappings) + +// v1ClaGroups.Projects = append(v1ClaGroups.Projects, s.collateFoundationCLAGroups(ctx, uniqueCLAGroupList)...) + +// return nil +// } + +func (s *service) appendCLAGroupsForProject(ctx context.Context, f logrus.Fields, projectOrFoundationSFID string, sfProjectModelDetails *v2ProjectServiceModels.ProjectOutputDetailed, v1ClaGroups *v1Models.ClaGroups) (string, string, error) { + // Since this is a project and not a foundation, we'll want to set he parent foundation ID and name (which is + // our parent in this case) + var foundationID, foundationName string + if utils.IsProjectHaveParent(sfProjectModelDetails) { + foundationID = sfProjectModelDetails.Foundation.ID + foundationName = sfProjectModelDetails.Foundation.Name + log.WithFields(f).Debugf("using parent foundation ID: %s and name: %s", foundationID, foundationName) + } else { + // Project with no parent - must be a standalone - use our ID and Name as the foundation + foundationID = sfProjectModelDetails.ID + foundationName = sfProjectModelDetails.Name + log.WithFields(f).Debugf("no parent - using project as foundation ID: %s and name: %s", foundationID, foundationName) + } - cclaSignatureDetails, err := s.signatureService.GetProjectSignatures(ctx, signatures.GetProjectSignaturesParams{ProjectID: responseEntry.ClaGroupID, ClaType: aws.String(utils.ClaTypeCCLA), SignatureType: aws.String(utils.SignatureTypeCCLA)}) - if err != nil { - log.Warnf("error while getting ICLA Signature using clagroupID %s Error: %v", responseEntry.ClaGroupID, err) - } - cclaSignatureCount = cclaSignatureDetails.ResultCount + log.WithFields(f).Debugf("locating CLA Group mapping using projectOrFoundationSFID: '%s'...", projectOrFoundationSFID) + projectCLAGroup, lookupErr := s.projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, projectOrFoundationSFID) - responseEntry.TotalSignatures = cclaSignatureCount + iclaSignatureCount + // in case project has no mapping check cla groups at the descendant level + if lookupErr != nil || projectCLAGroup == nil || projectCLAGroup.ClaGroupID == "" { + log.WithFields(f).WithError(lookupErr).Warnf("problem locating CLA group by project id: '%s'", projectOrFoundationSFID) } - // Sort the response based on the Foundation and CLA group name - sort.Slice(responseModel.List, func(i, j int) bool { - switch strings.Compare(responseModel.List[i].FoundationName, responseModel.List[j].FoundationName) { - case -1: - return true - case 1: - return false + if projectCLAGroup != nil && projectCLAGroup.ClaGroupID != "" { + log.WithFields(f).Debugf("loading CLA Group by ID: '%s' - %+v", projectCLAGroup.ClaGroupID, projectCLAGroup) + v1ClaGroupsByProject, claGroupLoadErr := s.v1ProjectService.GetCLAGroupByID(ctx, projectCLAGroup.ClaGroupID) + if claGroupLoadErr != nil { + log.WithFields(f).Warnf("problem loading CLA group by id: '%s', error: %+v", projectCLAGroup.ClaGroupID, claGroupLoadErr) + return "", "", &utils.CLAGroupNotFound{CLAGroupID: projectCLAGroup.ClaGroupID, Err: claGroupLoadErr} } - return responseModel.List[i].ClaGroupName < responseModel.List[j].ClaGroupName - }) - return responseModel, nil + v1ClaGroups.Projects = append(v1ClaGroups.Projects, *v1ClaGroupsByProject) + } + + psc := v2ProjectService.GetClient() + + projectSummary, err := psc.GetSummary(ctx, projectOrFoundationSFID) + if err != nil { + log.WithFields(f).Warnf("problem loading project summary by id: '%s', error: %+v", projectOrFoundationSFID, err) + return "", "", err + } + + log.WithFields(f).Debugf("Getting child projects for project: %s", projectOrFoundationSFID) + + childProjects := GetProjectDescendants(projectSummary) + log.WithFields(f).Debugf("project descendant list: %+v", childProjects) + + var wg sync.WaitGroup + wg.Add(len(childProjects)) + + for _, childProject := range childProjects { + go func(childProject string) { + defer wg.Done() + log.WithFields(f).Debugf("Getting CLA Group for child project: %s", childProject) + claData, v1ClaGroupErr := s.v1ProjectService.GetClaGroupByProjectSFID(ctx, childProject, false) + if v1ClaGroupErr != nil { + log.WithFields(f).Warnf("problem locating CLA group by project id, error: %+v", v1ClaGroupErr) + return + } + if claData.ProjectID == "" { + log.WithFields(f).Warnf("problem locating CLA group by project id: %s", childProject) + } else { + log.WithFields(f).Debugf("loading CLA Group by ID: '%s' - %+v", claData.ProjectID, claData) + } + _, found := Find(v1ClaGroups.Projects, claData.ProjectID) + if !found { + v1ClaGroups.Projects = append(v1ClaGroups.Projects, *claData) + } + }(childProject) + } + + wg.Wait() + + return foundationID, foundationName, nil } func (s *service) ListAllFoundationClaGroups(ctx context.Context, foundationID *string) (*models.FoundationMappingList, error) { f := logrus.Fields{ - "functionName": "ListAllFoundationClaGroups", + "functionName": "v2.cla_groups.service.ListAllFoundationClaGroups", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "foundationID": foundationID, } @@ -590,9 +731,9 @@ func (s *service) ListAllFoundationClaGroups(ctx context.Context, foundationID * var out []*projects_cla_groups.ProjectClaGroup var err error if foundationID != nil { - out, err = s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(*foundationID) + out, err = s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(ctx, utils.StringValue(foundationID)) } else { - out, err = s.projectsClaGroupsRepo.GetProjectsIdsForAllFoundation() + out, err = s.projectsClaGroupsRepo.GetProjectsIdsForAllFoundation(ctx) } if err != nil { return nil, err @@ -603,7 +744,7 @@ func (s *service) ListAllFoundationClaGroups(ctx context.Context, foundationID * // DeleteCLAGroup handles deleting and invalidating the CLA group, removing permissions, cleaning up pending requests, etc. func (s *service) DeleteCLAGroup(ctx context.Context, claGroupModel *v1Models.ClaGroup, authUser *auth.User) error { f := logrus.Fields{ - "functionName": "DeleteCLAGroup", + "functionName": "v2.cla_groups.service.DeleteCLAGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupModel.ProjectID, "claGroupExternalID": claGroupModel.ProjectExternalID, @@ -619,7 +760,7 @@ func (s *service) DeleteCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl oscClient := organization_service.GetClient() // Get a list of project CLA Group entries - need to know which SF Projects we're dealing with... - projectCLAGroupEntries, projErr := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(claGroupModel.ProjectID) + projectCLAGroupEntries, projErr := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, claGroupModel.ProjectID) if projErr != nil { log.WithFields(f).Warnf("unable to fetch project IDs for CLA Group, error: %+v", projErr) return projErr @@ -669,6 +810,7 @@ func (s *service) DeleteCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl s.eventsService.LogEvent(&events.LogEventArgs{ EventType: events.GerritRepositoryDeleted, ClaGroupModel: claGroup, + CLAGroupID: claGroup.ProjectID, LfUsername: authUser.UserName, EventData: &events.GerritProjectDeletedEventData{ DeletedCount: numDeleted, @@ -835,10 +977,20 @@ func (s *service) DeleteCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl } } - // Associate the specified projects with our new CLA Group - err := s.UnenrollProjectsInClaGroup(ctx, claGroupModel.ProjectID, foundationSFID, projectIDList.List()) + unenrollModel := UnenrollProjectsModel{ + AuthUser: authUser, + CLAGroupID: claGroupModel.ProjectID, + FoundationSFID: foundationSFID, + ProjectSFIDList: projectIDList.List(), + } + + log.WithFields(f).Debugf("Unenrolling with request: %+v", unenrollModel) + + // Unenroll the specified projects with the CLA Group + err := s.UnenrollProjectsInClaGroup(ctx, &unenrollModel) if err != nil { log.WithFields(f).WithError(err).Warn("unenrolling projects in CLA Group failed - manual cleanup required.") + return err } // Finally, delete the CLA Group last... @@ -853,20 +1005,33 @@ func (s *service) DeleteCLAGroup(ctx context.Context, claGroupModel *v1Models.Cl } // EnrollProjectsInClaGroup enrolls the specified project list in the CLA Group -func (s *service) EnrollProjectsInClaGroup(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error { +func (s *service) EnrollProjectsInClaGroup(ctx context.Context, request *EnrollProjectsModel) error { f := logrus.Fields{ - "functionName": "EnrollProjectsInClaGroup", + "functionName": "v2.cla_groups.service.EnrollProjectsInClaGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "foundationSFID": foundationSFID, - "projectSFIDList": strings.Join(projectSFIDList, ","), + "authUserName": request.AuthUser.UserName, + "authUserEmail": request.AuthUser.Email, + "claGroupID": request.CLAGroupID, + "foundationSFID": request.FoundationSFID, + "projectSFIDList": strings.Join(request.ProjectSFIDList, ","), + } + + // We have two options - ask the UI to strip out any LF parent projects or we do it here + // In the interest of time, we'll do it here + psc := v2ProjectService.GetClient() + updatedProjectSFIDList := psc.RemoveLinuxFoundationParentsFromProjectList(request.ProjectSFIDList) + if len(updatedProjectSFIDList) != len(request.ProjectSFIDList) { + log.WithFields(f).Debugf("removed %d linux foundation parent projects from project list", len(request.ProjectSFIDList)-len(updatedProjectSFIDList)) } log.WithFields(f).Debug("validating enroll project input") - err := s.validateEnrollProjectsInput(ctx, foundationSFID, projectSFIDList) + err := s.validateEnrollProjectsInput(ctx, request.FoundationSFID, updatedProjectSFIDList, request.ProjectLevel, request.CLAGroupProjects) if err != nil { - log.WithFields(f).Warnf("validating enroll project input failed. error = %s", err) - return err + return &utils.EnrollValidationError{ + Type: "enroll", + Message: "invalid project ID value", + Err: err, + } } // Setup a wait group to enroll and enable CLA service - we'll want to work quickly here @@ -875,52 +1040,66 @@ func (s *service) EnrollProjectsInClaGroup(ctx context.Context, claGroupID strin wg.Add(2) // Separate go routine for enrolling projects - go func(c context.Context, claGrID string, fSFID string, projSFIDList []string) { + go func(c context.Context, authUser *auth.User, claGroupID string, foundationSFID string, projSFIDList []string) { defer wg.Done() log.WithFields(f).Debug("enrolling projects in CLA Group") - enrollErr := s.AssociateCLAGroupWithProjects(c, claGrID, fSFID, projSFIDList) + enrollErr := s.AssociateCLAGroupWithProjects(c, &AssociateCLAGroupWithProjectsModel{ + AuthUser: authUser, + CLAGroupID: claGroupID, + FoundationSFID: foundationSFID, + ProjectSFIDList: projSFIDList, + }) if enrollErr != nil { log.WithFields(f).WithError(enrollErr).Warn("enrolling projects in CLA Group failed") errorList = append(errorList, enrollErr) } - - }(ctx, claGroupID, foundationSFID, projectSFIDList) + }(ctx, request.AuthUser, request.CLAGroupID, request.FoundationSFID, updatedProjectSFIDList) // Separate go routine for enabling the CLA Service in the project service - go func(c context.Context, projSFIDList []string) { + go func(c context.Context, claGroupID string, projSFIDList []string) { defer wg.Done() log.WithFields(f).Debug("enabling CLA service in platform project service") - errEnableCLA := s.EnableCLAService(c, projSFIDList) + // Note: log entry will be created by enable CLA Service call + errEnableCLA := s.EnableCLAService(c, request.AuthUser, claGroupID, projSFIDList) if errEnableCLA != nil { log.WithFields(f).WithError(errEnableCLA).Warn("enabling CLA service in platform project service failed") errorList = append(errorList, errEnableCLA) } - }(ctx, projectSFIDList) + }(ctx, request.CLAGroupID, updatedProjectSFIDList) // Wait until all go routines are done wg.Wait() if len(errorList) > 0 { - log.WithFields(f).WithError(errorList[0]).Warnf("encountered %d errors when enrolling and enabling CLA service for %d projects", len(errorList), len(projectSFIDList)) - return errorList[0] + return &utils.EnrollError{ + Type: "enroll", + Message: fmt.Sprintf("encountered %d errors when enrolling and disabling CLA service for %d projects", len(errorList), len(updatedProjectSFIDList)), + Err: errorList[0], + } } return nil } -func (s *service) UnenrollProjectsInClaGroup(ctx context.Context, claGroupID string, foundationSFID string, projectSFIDList []string) error { +// UnenrollProjectsInClaGroup un-enrolls the specified projects from the CLA group +func (s *service) UnenrollProjectsInClaGroup(ctx context.Context, request *UnenrollProjectsModel) error { f := logrus.Fields{ - "functionName": "UnenrollProjectsInClaGroup", + "functionName": "v2.cla_groups.service.UnenrollProjectsInClaGroup", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "claGroupID": claGroupID, - "foundationSFID": foundationSFID, - "projectSFIDList": strings.Join(projectSFIDList, ","), + "authUserName": request.AuthUser.UserName, + "authUserEmail": request.AuthUser.Email, + "claGroupID": request.CLAGroupID, + "foundationSFID": request.FoundationSFID, + "projectSFIDList": strings.Join(request.ProjectSFIDList, ","), } log.WithFields(f).Debug("validating unenroll project input") - err := s.validateUnenrollProjectsInput(ctx, foundationSFID, projectSFIDList) + err := s.validateUnenrollProjectsInput(ctx, request.FoundationSFID, request.ProjectSFIDList) if err != nil { - log.WithFields(f).Warnf("validating unenroll project input failed. error = %s", err) - return err + return &utils.EnrollValidationError{ + Type: "unenroll", + Message: "invalid project ID value", + Err: err, + } } // Setup a wait group to enroll and enable CLA service - we'll want to work quickly here @@ -928,34 +1107,41 @@ func (s *service) UnenrollProjectsInClaGroup(ctx context.Context, claGroupID str var wg sync.WaitGroup wg.Add(2) - // Separate go routine for unenrolling projects - go func(c context.Context, claGrID string, fSFID string, projSFIDList []string) { + // Separate go routine for un-enrolling projects + go func(c context.Context, authUser *auth.User, claGroupID string, foundationSFID string, projSFIDList []string) { defer wg.Done() log.WithFields(f).Debug("unenrolling projects in CLA Group") - unenrollErr := s.UnassociateCLAGroupWithProjects(c, claGrID, fSFID, projSFIDList) + unenrollErr := s.UnassociateCLAGroupWithProjects(c, &UnassociateCLAGroupWithProjectsModel{ + AuthUser: authUser, + CLAGroupID: claGroupID, + FoundationSFID: foundationSFID, + ProjectSFIDList: projSFIDList, + }) if unenrollErr != nil { log.WithFields(f).WithError(unenrollErr).Warn("unenrolling projects in CLA Group failed") errorList = append(errorList, unenrollErr) } - - }(ctx, claGroupID, foundationSFID, projectSFIDList) + }(ctx, request.AuthUser, request.CLAGroupID, request.FoundationSFID, request.ProjectSFIDList) // Separate go routine for disabling the CLA Service in the project service - go func(c context.Context, projSFIDList []string) { + go func(c context.Context, claGroupID string, projSFIDList []string) { defer wg.Done() - log.WithFields(f).Debug("disabling CLA service in platform project service") - errDisableCLA := s.DisableCLAService(c, projSFIDList) + // Note: log entry will be created by disable CLA Service call + errDisableCLA := s.DisableCLAService(c, request.AuthUser, claGroupID, projSFIDList) if errDisableCLA != nil { log.WithFields(f).WithError(errDisableCLA).Warn("disabling CLA service in platform project service failed") errorList = append(errorList, errDisableCLA) } - }(ctx, projectSFIDList) + }(ctx, request.CLAGroupID, request.ProjectSFIDList) // Wait until all go routines are done wg.Wait() if len(errorList) > 0 { - log.WithFields(f).WithError(errorList[0]).Warnf("encountered %d errors when unenrolling and disabling CLA service for %d projects", len(errorList), len(projectSFIDList)) - return errorList[0] + return &utils.EnrollError{ + Type: "unenroll", + Message: fmt.Sprintf("encountered %d errors when unenrolling and disabling CLA service for %d projects", len(errorList), len(request.ProjectSFIDList)), + Err: errorList[0], + } } return nil diff --git a/cla-backend-go/v2/cla_manager/emails.go b/cla-backend-go/v2/cla_manager/emails.go new file mode 100644 index 000000000..96828ac0f --- /dev/null +++ b/cla-backend-go/v2/cla_manager/emails.go @@ -0,0 +1,381 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package cla_manager + +import ( + "context" + "fmt" + "strings" + + v2AcsService "github.com/communitybridge/easycla/cla-backend-go/v2/acs-service" + + "github.com/communitybridge/easycla/cla-backend-go/emails" + v1Models "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/sirupsen/logrus" +) + +// EmailToCLAManagerModel data model for sending emails to CLA Managers +type EmailToCLAManagerModel struct { + Contributor *v1Models.User + CLAManagerName string + CLAManagerEmail string + CompanyName string + CLAGroupName string + CorporateConsoleURL string +} + +// ToCLAManagerDesigneeCorporateModel data model for sending emails +type ToCLAManagerDesigneeCorporateModel struct { + companyName string + projectSFID string + projectName string + designeeEmail string + designeeName string + senderEmail string + senderName string +} + +// ToCLAManagerDesigneeModel data model for sending emails +type ToCLAManagerDesigneeModel struct { + designeeName string + designeeEmail string + companyName string + projectNames []string + projectSFIDs []string + contributorModel emails.Contributor +} + +// DesigneeEmailToUserWithNoLFIDModel data model for sending emails +type DesigneeEmailToUserWithNoLFIDModel struct { + userWithNoLFIDName string + userWithNoLFIDEmail string + contributorModel emails.Contributor + projectNames []string + projectSFIDs []string + foundationSFID string + role string + companyName string + organizationID string +} + +// EmailToUserWithNoLFIDModel data model for sending emails +type EmailToUserWithNoLFIDModel struct { + projectName string + requesterUsername string + requesterEmail string + userWithNoLFIDName string + userWithNoLFIDEmail string + organizationID string + companyName string + projectID string + role string +} + +// EmailToOrgAdminModel data model for sending emails +type EmailToOrgAdminModel struct { + adminEmail string + adminName string + companyName string + projectName string + projectSFID string + senderName string + senderEmail string +} + +// ContributorEmailToOrgAdminModel data model for sending emails +type ContributorEmailToOrgAdminModel struct { + adminEmail string + adminName string + companyName string + projectSFIDs []string + contributor *v1Models.User + userDetails string +} + +// SendEmailToCLAManager handles sending an email to the specified CLA Manager +func (s *service) SendEmailToCLAManager(ctx context.Context, input *EmailToCLAManagerModel, projectSFIDs []string) { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendEmailToCLAManager", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "contributorUsername": input.Contributor.Username, + "contributorLFUsername": input.Contributor.LfUsername, + "contributorGitHubID": input.Contributor.GithubID, + "contributorGitHubUsername": input.Contributor.GithubUsername, + "contributorLFEmail": input.Contributor.LfEmail, + "contributorEmails": strings.Join(input.Contributor.Emails, ","), + "claManagerName": input.CLAManagerName, + "claManagerEmail": input.CLAManagerEmail, + "companyName": input.CompanyName, + "claGroupName": input.CLAGroupName, + } + + subject := fmt.Sprintf("EasyCLA: Approval Request for contributor: %s", getBestUserName(input.Contributor)) + recipients := []string{input.CLAManagerEmail} + body, err := emails.RenderV2ContributorApprovalRequestTemplate(s.emailTemplateService, projectSFIDs, emails.V2ContributorApprovalRequestTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.CLAManagerName, + CompanyName: input.CompanyName, + }, + SigningEntityName: input.CompanyName, + UserDetails: getFormattedUserDetails(input.Contributor), + }) + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering email template: %s", emails.V2ContributorApprovalRequestTemplateName) + return + } + + log.WithFields(f).Debugf("sending email with subject: %s to recipients: %+v...", subject, recipients) + 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) + } +} + +// SendEmailToOrgAdmin sends an email to the organization admin +func (s *service) SendEmailToOrgAdmin(ctx context.Context, input EmailToOrgAdminModel) { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendEmailToOrgAdmin", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "adminEmail": input.adminEmail, + "adminName": input.adminName, + "companyName": input.companyName, + "projectName": input.projectName, + "projectSFID": input.projectSFID, + "senderName": input.senderName, + "senderEmail": input.senderEmail, + } + + subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA ", input.companyName) + recipients := []string{input.adminEmail} + body, err := emails.RenderV2OrgAdminTemplate(s.emailTemplateService, input.projectSFID, emails.V2OrgAdminTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.adminName, + CompanyName: input.companyName, + }, + SenderName: input.senderName, + SenderEmail: input.senderEmail, + }) + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering email template : %s failed : %v", emails.V2OrgAdminTemplateName, err) + return + } + 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 (s *service) ContributorEmailToOrgAdmin(ctx context.Context, input ContributorEmailToOrgAdminModel) { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendEmailToOrgAdmin", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "adminEmail": input.adminEmail, + "adminName": input.adminName, + "companyName": input.companyName, + "contributorName": input.contributor.Username, + "contributorGitHubID": input.contributor.GithubID, + "contributorGitHubUsername": input.contributor.GithubUsername, + "contributorLFUsername": input.contributor.LfUsername, + "contributorLFEmail": input.contributor.LfEmail, + "contributorEmails": strings.Join(input.contributor.Emails, ","), + } + + subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA and add to approved list %s ", input.companyName, getBestUserName(input.contributor)) + recipients := []string{input.adminEmail} + body, err := emails.RenderV2ContributorToOrgAdminTemplate(s.emailTemplateService, input.projectSFIDs, emails.V2ContributorToOrgAdminTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.adminName, + CompanyName: input.companyName, + }, + UserDetails: input.userDetails, + }) + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering template : %s failed : %v", emails.V2ContributorToOrgAdminTemplateName, err) + return + } + 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 (s *service) SendEmailToCLAManagerDesigneeCorporate(ctx context.Context, input ToCLAManagerDesigneeCorporateModel) { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendEmailToCLAManagerDesigneeCorporate", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": input.companyName, + "projectName": input.projectName, + "designeeEmail": input.designeeEmail, + "designeeName": input.designeeName, + "senderEmail": input.senderEmail, + "senderName": input.senderName, + } + + subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA ", input.companyName) + recipients := []string{input.designeeEmail} + body, err := emails.RenderV2CLAManagerDesigneeCorporateTemplate(s.emailTemplateService, input.projectSFID, emails.V2CLAManagerDesigneeCorporateTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.designeeName, + CompanyName: input.companyName, + }, + SenderName: input.senderName, + SenderEmail: input.senderEmail, + }) + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering template : %s : failed: %v", emails.V2CLAManagerDesigneeCorporateTemplateName, err) + return + } + 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 (s *service) SendEmailToCLAManagerDesignee(ctx context.Context, input ToCLAManagerDesigneeModel) { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendEmailToCLAManagerDesignee", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": input.companyName, + "projectNames": strings.Join(input.projectNames, ","), + "designeeEmail": input.designeeEmail, + "designeeName": input.designeeName, + "contributorEmail": input.contributorModel.Email, + "contributorName": input.contributorModel.Username, + } + + subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA and add to approved list %s ", + input.companyName, input.contributorModel.Email) + recipients := []string{input.designeeEmail} + body, err := emails.RenderV2ToCLAManagerDesigneeTemplate(s.emailTemplateService, input.projectSFIDs, + emails.V2ToCLAManagerDesigneeTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.designeeName, + CompanyName: input.companyName, + }, + Contributor: input.contributorModel, + }, emails.V2ToCLAManagerDesigneeTemplate, emails.V2ToCLAManagerDesigneeTemplateName) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering template : %s failed : %v", emails.V2ToCLAManagerDesigneeTemplateName, err) + return + } + 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 (s *service) SendDesigneeEmailToUserWithNoLFID(ctx context.Context, input DesigneeEmailToUserWithNoLFIDModel) error { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendDesigneeEmailToUserWithNoLFID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "userWithNoLFIDName": input.userWithNoLFIDName, + "userWithNoLFIDEmail": input.userWithNoLFIDEmail, + "organizationID": input.organizationID, + "projectNames": strings.Join(input.projectNames, ","), + "role": input.role, + "requesterUsername": input.contributorModel.Username, + "requesterEmail": input.contributorModel.Email, + } + + subject := "EasyCLA: Invitation to create LF Login and complete process of becoming CLA Manager" + + body, err := emails.RenderV2ToCLAManagerDesigneeTemplate(s.emailTemplateService, input.projectSFIDs, + emails.V2ToCLAManagerDesigneeTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.userWithNoLFIDName, + CompanyName: input.companyName, + }, + Contributor: input.contributorModel, + }, emails.V2DesigneeToUserWithNoLFIDTemplate, emails.V2DesigneeToUserWithNoLFIDTemplateName) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering template : %s failed : %v", emails.V2DesigneeToUserWithNoLFIDTemplateName, err) + return err + } + + acsClient := v2AcsService.GetClient() + log.WithFields(f).Debug("sending user invite request...") + + // Parse the provided user's name + userFirstName, userLastName := utils.GetFirstAndLastName(input.userWithNoLFIDName) + + return acsClient.SendUserInvite(ctx, &v2AcsService.SendUserInviteInput{ + InviteUserFirstName: userFirstName, + InviteUserLastName: userLastName, + InviteUserEmail: input.userWithNoLFIDEmail, + RoleName: input.role, + Scope: utils.ProjectOrgScope, + ProjectSFID: input.foundationSFID, + OrganizationSFID: input.organizationID, + InviteType: "userinvite", + Subject: subject, + EmailContent: body, + Automate: false, + }) +} + +// sendEmailToUserWithNoLFID helper function to send email to a given user with no LFID +func (s *service) SendEmailToUserWithNoLFID(ctx context.Context, input EmailToUserWithNoLFIDModel) error { + f := logrus.Fields{ + "functionName": "cla_manager.service.SendEmailToUserWithNoLFID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectName": input.projectName, + "requesterUsername": input.requesterUsername, + "requesterEmail": input.requesterEmail, + "userWithNoLFIDName": input.userWithNoLFIDName, + "userWithNoLFIDEmail": input.userWithNoLFIDEmail, + "organizationID": input.organizationID, + "projectID": input.projectID, + "role": input.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", input.role) + body, err := emails.RenderV2CLAManagerToUserWithNoLFIDTemplate(s.emailTemplateService, input.projectID, emails.V2CLAManagerToUserWithNoLFIDTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: input.userWithNoLFIDName, + CompanyName: input.companyName, + }, + RequesterUserName: input.requesterUsername, + RequesterEmail: input.requesterEmail, + }) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("rendering email : %s failed : %v", emails.V2CLAManagerToUserWithNoLFIDTemplateName, err) + return err + } + acsClient := v2AcsService.GetClient() + + // Parse the provided user's name + userFirstName, userLastName := utils.GetFirstAndLastName(input.userWithNoLFIDName) + + log.WithFields(f).Debug("sending user invite request...") + //return acsClient.SendUserInvite(ctx, &userWithNoLFIDEmail, role, utils.ProjectOrgScope, projectID, organizationID, "userinvite", &subject, &body, automate) + return acsClient.SendUserInvite(ctx, &v2AcsService.SendUserInviteInput{ + InviteUserFirstName: userFirstName, + InviteUserLastName: userLastName, + InviteUserEmail: input.userWithNoLFIDEmail, + RoleName: input.role, + Scope: utils.ProjectOrgScope, + ProjectSFID: input.projectID, + OrganizationSFID: input.organizationID, + InviteType: "userinvite", + Subject: subject, + EmailContent: body, + Automate: false, + }) +} diff --git a/cla-backend-go/v2/cla_manager/errors.go b/cla-backend-go/v2/cla_manager/errors.go new file mode 100644 index 000000000..13bf369d6 --- /dev/null +++ b/cla-backend-go/v2/cla_manager/errors.go @@ -0,0 +1,39 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package cla_manager + +import ( + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/cla_manager" +) + +// buildErrorMessageCreate helper function to build an error message +func buildErrorMessageCreate(params cla_manager.CreateCLAManagerParams, err error) string { + return fmt.Sprintf("problem creating new CLA Manager using company ID: %s, project SFID: %s, firstName: %s, lastName: %s, user email: %s, error: %+v", + params.CompanyID, params.ProjectSFID, *params.Body.FirstName, *params.Body.LastName, *params.Body.UserEmail, err) +} + +// buildErrorMessage helper function to build an error message +func buildErrorMessageDelete(params cla_manager.DeleteCLAManagerParams, err error) string { + return fmt.Sprintf("problem deleting new CLA Manager Request using company ID: %s, project SFID: %s, user ID: %s, error: %+v", + params.CompanyID, params.ProjectSFID, params.UserLFID, err) +} + +// buildErrorStatusCode helper function to build an error statusCodes +func buildErrorStatusCode(err error) string { + if err == ErrNoOrgAdmins || err == ErrCLACompanyNotFound || err == ErrClaGroupNotFound || err == ErrCLAUserNotFound { + return NotFound + } + // Check if user is already assigned scope/role + if err == ErrRoleScopeConflict { + return Conflict + } + // Check if user does exists + if err == ErrNoLFID { + return Accepted + } + // Return Bad Request + return BadRequest +} diff --git a/cla-backend-go/v2/cla_manager/handlers.go b/cla-backend-go/v2/cla_manager/handlers.go index 8eb699b1e..20f169043 100644 --- a/cla-backend-go/v2/cla_manager/handlers.go +++ b/cla-backend-go/v2/cla_manager/handlers.go @@ -15,6 +15,7 @@ import ( "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/cla_manager" @@ -36,35 +37,48 @@ const ( ) // Configure is the API handler routine for CLA Manager routes -func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, projectClaGroupRepo projects_cla_groups.Repository, easyCLAUserRepo v1User.RepositoryService) { +func Configure(api *operations.EasyclaAPI, service Service, v1CompanyService v1Company.IService, LfxPortalURL, CorporateConsoleV2URL string, projectClaGroupRepo projects_cla_groups.Repository, easyCLAUserRepo v1User.RepositoryService) { // nolint api.ClaManagerCreateCLAManagerHandler = cla_manager.CreateCLAManagerHandlerFunc(func(params cla_manager.CreateCLAManagerParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - if !utils.IsUserAuthorizedForProjectOrganizationTree(authUser, params.ProjectSFID, params.CompanySFID, utils.DISALLOW_ADMIN_SCOPE) { - return cla_manager.NewCreateCLAManagerForbidden().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, - }) + + f := logrus.Fields{ + "functionName": "v2.cla_manager.handlers.ClaManagerCreateCLAManagerHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "CompanyID": params.CompanyID, + "ProjectSFID": params.ProjectSFID, + "authUser": *params.XUSERNAME, + } + + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v1CompanyModel, err := v1CompanyService.GetCompany(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) + return cla_manager.NewCreateCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + log.WithFields(f).Debug("checking permissions...") + if !utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, params.ProjectSFID, v1CompanyModel.CompanyExternalID, utils.DISALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to DeleteCLAManager with Project|Organization scope of %s | %s", authUser.UserName, params.ProjectSFID, params.CompanyID) + log.WithFields(f).Warn(msg) + return cla_manager.NewCreateCLAManagerForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - cginfo, err := projectClaGroupRepo.GetClaGroupIDForProject(params.ProjectSFID) + + log.WithFields(f).Debug("looking up CLA Group for projectSFID...") + cginfo, err := projectClaGroupRepo.GetClaGroupIDForProject(ctx, params.ProjectSFID) if err != nil { if err == projects_cla_groups.ErrProjectNotAssociatedWithClaGroup { - return cla_manager.NewCreateCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: BadRequest, - Message: fmt.Sprintf("EasyCLA - 400 Bad Request - No cla group associated with this project: %s", params.ProjectSFID), - XRequestID: reqID, - }) + msg := fmt.Sprintf("no CLA Group associated with this project: %s", params.ProjectSFID) + log.WithFields(f).WithError(err).Warn(msg) + return cla_manager.NewCreateCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - return cla_manager.NewCreateCLAManagerInternalServerError().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "500", - Message: fmt.Sprintf("EasyCLA - 500 Internal server error. error = %s", err.Error()), - XRequestID: reqID, - }) + return cla_manager.NewCreateCLAManagerInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, err.Error(), err)) } - compCLAManager, errorResponse := service.CreateCLAManager(ctx, cginfo.ClaGroupID, params, authUser.UserName) + + compCLAManager, errorResponse := service.CreateCLAManager(ctx, authUser, cginfo.ClaGroupID, params, authUser.UserName) if errorResponse != nil { if errorResponse.Code == BadRequest { return cla_manager.NewCreateCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(errorResponse) @@ -78,26 +92,41 @@ func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, api.ClaManagerDeleteCLAManagerHandler = cla_manager.DeleteCLAManagerHandlerFunc(func(params cla_manager.DeleteCLAManagerParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - if !utils.IsUserAuthorizedForProjectOrganizationTree(authUser, params.ProjectSFID, params.CompanySFID, utils.DISALLOW_ADMIN_SCOPE) { - return cla_manager.NewDeleteCLAManagerForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to DeleteCLAManager with Project|Organization scope of %s | %s", - authUser.UserName, params.ProjectSFID, params.CompanySFID), - XRequestID: reqID, - }) + f := logrus.Fields{ + "functionName": "v2.cla_manager.handlers.ClaManagerDeleteCLAManagerHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "CompanyID": params.CompanyID, + "ProjectSFID": params.ProjectSFID, + "userLFID": params.UserLFID, + "authUser": *params.XUSERNAME, + } + + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v1CompanyModel, err := v1CompanyService.GetCompany(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) + return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - cginfo, err := projectClaGroupRepo.GetClaGroupIDForProject(params.ProjectSFID) + + log.WithFields(f).Debug("checking permissions...") + if !utils.IsUserAuthorizedForProjectOrganizationTree(ctx, authUser, params.ProjectSFID, v1CompanyModel.CompanyExternalID, utils.DISALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to DeleteCLAManager with Project|Organization scope of %s | %s", authUser.UserName, params.ProjectSFID, params.CompanyID) + log.WithFields(f).Warn(msg) + return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + + cginfo, err := projectClaGroupRepo.GetClaGroupIDForProject(ctx, params.ProjectSFID) if err != nil { - return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: BadRequest, - Message: fmt.Sprintf("EasyCLA - Bad Request. No Cla Group associated with ProjectSFID: %s ", params.ProjectSFID), - XRequestID: reqID, - }) + msg := fmt.Sprintf("no CLA Group associated with this project: %s", params.ProjectSFID) + log.WithFields(f).WithError(err).Warn(msg) + return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - errResponse := service.DeleteCLAManager(ctx, cginfo.ClaGroupID, params) + errResponse := service.DeleteCLAManager(ctx, authUser, cginfo.ClaGroupID, params) if errResponse != nil { return cla_manager.NewDeleteCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(errResponse) } @@ -107,36 +136,36 @@ func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, api.ClaManagerCreateCLAManagerDesigneeHandler = cla_manager.CreateCLAManagerDesigneeHandlerFunc(func(params cla_manager.CreateCLAManagerDesigneeParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaManagerCreateCLAManagerDesigneeHandler", + "functionName": "v2.cla_manager.handlers.ClaManagerCreateCLAManagerDesigneeHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "CompanySFID": params.CompanySFID, + "CompanyID": params.CompanyID, "ProjectSFID": params.ProjectSFID, "authUser": *params.XUSERNAME, } + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v1CompanyModel, err := v1CompanyService.GetCompany(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) + return cla_manager.NewCreateCLAManagerDesigneeBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + // Note: anyone create assign a CLA manager designee...no permissions checks - log.WithFields(f).Debugf("processing CLA Manager Desginee request") + log.WithFields(f).Debugf("processing create CLA Manager Desginee request") utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - claManagerDesignee, err := service.CreateCLAManagerDesignee(ctx, params.CompanySFID, params.ProjectSFID, params.Body.UserEmail.String()) + claManagerDesignee, err := service.CreateCLAManagerDesignee(ctx, params.CompanyID, params.ProjectSFID, params.Body.UserEmail.String()) if err != nil { if err == ErrCLAManagerDesigneeConflict { msg := fmt.Sprintf("Conflict assigning cla manager role for Project SFID: %s ", params.ProjectSFID) - return cla_manager.NewCreateCLAManagerDesigneeByGroupConflict().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Message: msg, - Code: Conflict, - XRequestID: reqID, - }) + return cla_manager.NewCreateCLAManagerDesigneeByGroupConflict().WithXRequestID(reqID).WithPayload(utils.ErrorResponseConflictWithError(reqID, msg, err)) } msg := fmt.Sprintf("user :%s, error: %+v ", authUser.Email, err) - return cla_manager.NewCreateCLAManagerBadRequest().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Message: msg, - Code: BadRequest, - XRequestID: reqID, - }) + return cla_manager.NewCreateCLAManagerBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } log.Debugf("CLA Manager designee created : %+v", claManagerDesignee) @@ -146,66 +175,50 @@ func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, api.ClaManagerCreateCLAManagerDesigneeByGroupHandler = cla_manager.CreateCLAManagerDesigneeByGroupHandlerFunc( func(params cla_manager.CreateCLAManagerDesigneeByGroupParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaManagerCreateCLAManagerDesigneeByGroupHandler", + "functionName": "v2.cla_manager.handlers.ClaManagerCreateCLAManagerDesigneeByGroupHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "CompanySFID": params.CompanySFID, + "CompanySFID": params.CompanyID, "ClaGroupID": params.ClaGroupID, "Email": params.Body.UserEmail.String(), - "authUser": *params.XUSERNAME, + "authUser": utils.StringValue(params.XUSERNAME), } // Note: anyone create assign a CLA manager designee...no permissions checks log.WithFields(f).Debugf("processing CLA Manager Designee by group request") log.WithFields(f).Debugf("getting project IDs for CLA group") - projectCLAGroups, getErr := projectClaGroupRepo.GetProjectsIdsForClaGroup(params.ClaGroupID) + projectCLAGroups, getErr := projectClaGroupRepo.GetProjectsIdsForClaGroup(ctx, params.ClaGroupID) if getErr != nil { - msg := fmt.Sprintf("Error getting SF projects for claGroup: %s ", params.ClaGroupID) - log.WithFields(f).Warn(msg) + msg := fmt.Sprintf("error getting SF projects for claGroup: %s ", params.ClaGroupID) + log.WithFields(f).WithError(getErr).Warn(msg) return cla_manager.NewCreateCLAManagerDesigneeByGroupBadRequest().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Message: msg, - Code: BadRequest, - XRequestID: reqID, - }) + utils.ErrorResponseBadRequestWithError(reqID, msg, getErr)) } + log.WithFields(f).Debugf("found %d project IDs for CLA group", len(projectCLAGroups)) if len(projectCLAGroups) == 0 { msg := fmt.Sprintf("no projects associated with CLA Group: %s", params.ClaGroupID) log.WithFields(f).Warn(msg) - return cla_manager.NewCreateCLAManagerDesigneeByGroupNotFound().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Message: msg, - Code: BadRequest, - XRequestID: reqID, - }) - + return cla_manager.NewCreateCLAManagerDesigneeByGroupNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) } - designeeScopes, msg, err := service.CreateCLAManagerDesigneeByGroup(ctx, params, projectCLAGroups, f) + designeeScopes, msg, err := service.CreateCLAManagerDesigneeByGroup(ctx, params, projectCLAGroups) if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating cla manager designee for CLA Group: %s with user email: %s", params.ClaGroupID, params.Body.UserEmail) if err == ErrCLAManagerDesigneeConflict { - return cla_manager.NewCreateCLAManagerDesigneeByGroupConflict().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Message: msg, - Code: Conflict, - XRequestID: reqID, - }) + return cla_manager.NewCreateCLAManagerDesigneeByGroupConflict().WithXRequestID(reqID).WithPayload(utils.ErrorResponseConflictWithError(reqID, msg, err)) } - log.WithFields(f).Warn(msg) - return cla_manager.NewCreateCLAManagerDesigneeByGroupBadRequest().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Message: msg, - Code: BadRequest, - XRequestID: reqID, - }) + return cla_manager.NewCreateCLAManagerDesigneeByGroupBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } + return cla_manager.NewCreateCLAManagerDesigneeByGroupOK().WithXRequestID(reqID).WithPayload(&models.ClaManagerDesignees{ List: designeeScopes, }) }) + api.ClaManagerIsCLAManagerDesigneeHandler = cla_manager.IsCLAManagerDesigneeHandlerFunc(func(params cla_manager.IsCLAManagerDesigneeParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint @@ -227,19 +240,15 @@ func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, api.ClaManagerInviteCompanyAdminHandler = cla_manager.InviteCompanyAdminHandlerFunc(func(params cla_manager.InviteCompanyAdminParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + // Get Contributor details user, userErr := easyCLAUserRepo.GetUser(params.UserID) if userErr != nil { msg := fmt.Sprintf("Problem getting user by ID : %s, error: %+v ", params.UserID, userErr) - return cla_manager.NewInviteCompanyAdminBadRequest().WithXRequestID(reqID).WithPayload( - &models.ErrorResponse{ - Code: BadRequest, - Message: msg, - XRequestID: reqID, - }) + return cla_manager.NewInviteCompanyAdminBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, userErr)) } - claManagerDesignees, err := service.InviteCompanyAdmin(ctx, params.Body.ContactAdmin, params.Body.CompanyID, *params.Body.ClaGroupID, params.Body.UserEmail.String(), params.Body.Name, &user, LfxPortalURL) + claManagerDesignees, err := service.InviteCompanyAdmin(ctx, params.Body.ContactAdmin, params.Body.CompanyID, *params.Body.ClaGroupID, params.Body.UserEmail.String(), params.Body.Name, &user) if err != nil { statusCode := buildErrorStatusCode(err) @@ -287,27 +296,39 @@ func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, api.ClaManagerCreateCLAManagerRequestHandler = cla_manager.CreateCLAManagerRequestHandlerFunc(func(params cla_manager.CreateCLAManagerRequestParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) f := logrus.Fields{ - "functionName": "ClaManagerCreateCLAManagerRequestHandler", + "functionName": "v2.cla_manager.handlers.ClaManagerCreateCLAManagerRequestHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "CompanySFID": params.CompanySFID, + "CompanyID": params.CompanyID, "ProjectSFID": params.ProjectSFID, - "authUser": *params.XUSERNAME, + "contactAdmin": params.Body.ContactAdmin, + "userFullName": utils.StringValue(params.Body.FullName), + "userEmail": params.Body.UserEmail.String(), + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), } - log.WithFields(f).Debugf("processing CLA Manager request") - utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - if !utils.IsUserAuthorizedForOrganization(authUser, params.CompanySFID, utils.ALLOW_ADMIN_SCOPE) { - return cla_manager.NewCreateCLAManagerRequestForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to CreateCLAManagerRequest 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...") + v1CompanyModel, err := v1CompanyService.GetCompany(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) + return cla_manager.NewCreateCLAManagerRequestBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + // Check perms... + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, v1CompanyModel.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to CreateCLAManagerRequest with Project|Organization scope of %s | %s", + authUser.UserName, params.ProjectSFID, v1CompanyModel.CompanyExternalID) + log.WithFields(f).Warn(msg) + return cla_manager.NewCreateCLAManagerRequestForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } - claManagerDesignee, err := service.CreateCLAManagerRequest(ctx, params.Body.ContactAdmin, params.CompanySFID, params.ProjectSFID, params.Body.UserEmail.String(), - *params.Body.FullName, authUser, LfxPortalURL) + claManagerDesignee, err := service.CreateCLAManagerRequest(ctx, params.Body.ContactAdmin, v1CompanyModel.CompanyID, params.ProjectSFID, params.Body.UserEmail.String(), + *params.Body.FullName, authUser) if err != nil { statusCode := buildErrorStatusCode(err) @@ -347,48 +368,29 @@ func Configure(api *operations.EasyclaAPI, service Service, LfxPortalURL string, func(params cla_manager.NotifyCLAManagersParams) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - err := service.NotifyCLAManagers(ctx, params.Body, LfxPortalURL) + f := logrus.Fields{ + "functionName": "v2.cla_manager.handlers.ClaManagerNotifyCLAManagersHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": params.Body.CompanyName, + "signingEntityName": params.Body.SigningEntityName, + "userID": params.Body.UserID, + "claGroupName": params.Body.ClaGroupID, + } + log.WithFields(f).Debug("notifying CLA managers...") + err := service.NotifyCLAManagers(ctx, params.Body, CorporateConsoleV2URL) if err != nil { if err == ErrCLAUserNotFound { + msg := fmt.Sprintf("unable to notify cla managers - user not found: %s", params.Body.UserID) + log.WithFields(f).WithError(err).Warn(err) return cla_manager.NewNotifyCLAManagersNotFound().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseNotFound( - reqID, - fmt.Sprintf("unable to notify cla managers - user not found: %s", params.Body.UserID))) + utils.ErrorResponseNotFound(reqID, msg)) } - return cla_manager.NewNotifyCLAManagersBadRequest().WithXRequestID(reqID).WithPayload( - utils.ErrorResponseBadRequestWithError(reqID, fmt.Sprintf("unable to notify cla managers - cla group: %s, company: %s", params.Body.ClaGroupName, params.Body.CompanyName), err)) + + msg := fmt.Sprintf("unable to notify cla managers - cla group: %s, company: %s", params.Body.ClaGroupID, params.Body.CompanyName) + log.WithFields(f).WithError(err).Warn(err) + return cla_manager.NewNotifyCLAManagersBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } return cla_manager.NewNotifyCLAManagersNoContent().WithXRequestID(reqID) }) - -} - -// buildErrorMessageCreate helper function to build an error message -func buildErrorMessageCreate(params cla_manager.CreateCLAManagerParams, err error) string { - return fmt.Sprintf("problem creating new CLA Manager using company SFID: %s, project SFID: %s, firstName: %s, lastName: %s, user email: %s, error: %+v", - params.CompanySFID, params.ProjectSFID, *params.Body.FirstName, *params.Body.LastName, *params.Body.UserEmail, err) -} - -// buildErrorMessage helper function to build an error message -func buildErrorMessageDelete(params cla_manager.DeleteCLAManagerParams, err error) string { - return fmt.Sprintf("problem deleting new CLA Manager Request using company SFID: %s, project SFID: %s, user ID: %s, error: %+v", - params.CompanySFID, params.ProjectSFID, params.UserLFID, err) -} - -// buildErrorStatusCode helper function to build an error statusCodes -func buildErrorStatusCode(err error) string { - if err == ErrNoOrgAdmins || err == ErrCLACompanyNotFound || err == ErrClaGroupNotFound || err == ErrCLAUserNotFound { - return NotFound - } - // Check if user is already assigned scope/role - if err == ErrRoleScopeConflict { - return Conflict - } - // Check if user does not exiss - if err == ErrNoLFID { - return Accepted - } - // Return Bad Request - return BadRequest } diff --git a/cla-backend-go/v2/cla_manager/service.go b/cla-backend-go/v2/cla_manager/service.go index d22ed03fb..6664c853f 100644 --- a/cla-backend-go/v2/cla_manager/service.go +++ b/cla-backend-go/v2/cla_manager/service.go @@ -11,24 +11,27 @@ import ( "sync" "time" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + service2 "github.com/communitybridge/easycla/cla-backend-go/project/service" + "github.com/go-openapi/strfmt" "github.com/sirupsen/logrus" "github.com/LF-Engineering/lfx-kit/auth" + "github.com/communitybridge/easycla/cla-backend-go/emails" "github.com/communitybridge/easycla/cla-backend-go/events" "github.com/communitybridge/easycla/cla-backend-go/utils" "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/cla_manager" - "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/v2/organization-service/client/organizations" v1ClaManager "github.com/communitybridge/easycla/cla-backend-go/cla_manager" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" v1User "github.com/communitybridge/easycla/cla-backend-go/user" easyCLAUser "github.com/communitybridge/easycla/cla-backend-go/users" @@ -37,34 +40,23 @@ import ( v2OrgService "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" v2UserService "github.com/communitybridge/easycla/cla-backend-go/v2/user-service" - v2UserModels "github.com/communitybridge/easycla/cla-backend-go/v2/user-service/models" ) var ( - //ErrSalesForceProjectNotFound returned error if salesForce Project not found - ErrSalesForceProjectNotFound = errors.New("salesforce project not found") //ErrCLACompanyNotFound returned if EasyCLA company not found ErrCLACompanyNotFound = errors.New("company not found") - //ErrGitHubRepoNotFound returned if GH Repos is not found - ErrGitHubRepoNotFound = errors.New("github repo not found") //ErrCLAUserNotFound returned if EasyCLA User is not found ErrCLAUserNotFound = errors.New("cla user not found") - //ErrCLAManagersNotFound when cla managers arent found for given project and company - ErrCLAManagersNotFound = errors.New("cla managers not found") //ErrLFXUserNotFound when user-service fails to find user ErrLFXUserNotFound = errors.New("lfx user not found") //ErrNoLFID thrown when users dont have an LFID ErrNoLFID = errors.New("user has no LF Login") - //ErrNotInOrg when user is not in organization - ErrNotInOrg = errors.New("user not in organization") //ErrNoOrgAdmins when No admins found for organization ErrNoOrgAdmins = errors.New("no admins in company") //ErrRoleScopeConflict thrown if user already has role scope ErrRoleScopeConflict = errors.New("user is already cla-manager") //ErrCLAManagerDesigneeConflict when user is already assigned cla-manager-designee role ErrCLAManagerDesigneeConflict = errors.New("user already assigned cla-manager") - //ErrScopeNotFound returns error when getting scopeID - ErrScopeNotFound = errors.New("scope not found") //ErrProjectSigned returns error if project already signed ErrProjectSigned = errors.New("project already signed") //ErrClaGroupNotFound returns error if cla group not found @@ -74,66 +66,78 @@ var ( ) const ( - // NoAccount represents user with no company - NoAccount = "Individual - No Account" + // used for filtering when fetching contributor email + excludedNoReplyEmails = "noreply.github.com" ) type service struct { - companyService company.IService - projectService project.Service - repositoriesService repositories.Service - managerService v1ClaManager.IService - easyCLAUserService easyCLAUser.Service - v2CompanyService v2Company.Service - eventService events.Service - projectCGRepo projects_cla_groups.Repository + emailTemplateService emails.EmailTemplateService + companyService company.IService + projectService service2.Service + repositoriesService repositories.Service + managerService v1ClaManager.IService + easyCLAUserService easyCLAUser.Service + v2CompanyService v2Company.Service + eventService events.Service + projectCGRepo projects_cla_groups.Repository } // Service interface type Service interface { - CreateCLAManager(ctx context.Context, claGroupID string, params cla_manager.CreateCLAManagerParams, authUsername string) (*models.CompanyClaManager, *models.ErrorResponse) - DeleteCLAManager(ctx context.Context, claGroupID string, params cla_manager.DeleteCLAManagerParams) *models.ErrorResponse - InviteCompanyAdmin(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, name string, contributor *v1User.User, lFxPortalURL string) ([]*models.ClaManagerDesignee, error) + CreateCLAManager(ctx context.Context, authUser *auth.User, claGroupID string, params cla_manager.CreateCLAManagerParams, authUsername string) (*models.CompanyClaManager, *models.ErrorResponse) + DeleteCLAManager(ctx context.Context, authUser *auth.User, claGroupID string, params cla_manager.DeleteCLAManagerParams) *models.ErrorResponse + InviteCompanyAdmin(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, name string, contributor *v1User.User) ([]*models.ClaManagerDesignee, error) CreateCLAManagerDesignee(ctx context.Context, companyID string, projectID string, userEmail string) (*models.ClaManagerDesignee, error) - CreateCLAManagerRequest(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, fullName string, authUser *auth.User, LfxPortalURL string) (*models.ClaManagerDesignee, error) - NotifyCLAManagers(ctx context.Context, notifyCLAManagers *models.NotifyClaManagerList, LfxPortalURL string) error - CreateCLAManagerDesigneeByGroup(ctx context.Context, params cla_manager.CreateCLAManagerDesigneeByGroupParams, projectCLAGroups []*projects_cla_groups.ProjectClaGroup, f logrus.Fields) ([]*models.ClaManagerDesignee, string, error) + CreateCLAManagerRequest(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, fullName string, authUser *auth.User) (*models.ClaManagerDesignee, error) + NotifyCLAManagers(ctx context.Context, notifyCLAManagers *models.NotifyClaManagerList, CorporateConsoleV2URL string) error + CreateCLAManagerDesigneeByGroup(ctx context.Context, params cla_manager.CreateCLAManagerDesigneeByGroupParams, projectCLAGroups []*projects_cla_groups.ProjectClaGroup) ([]*models.ClaManagerDesignee, string, error) + ProjectCompanySignedOrNot(ctx context.Context, signedAtFoundation bool, projectCLAGroups []*projects_cla_groups.ProjectClaGroup, companyModel *v1Models.Company) error IsCLAManagerDesignee(ctx context.Context, companySFID, claGroupID, userLFID string) (*models.UserRoleStatus, error) + + // Email Functions + SendEmailToCLAManager(ctx context.Context, input *EmailToCLAManagerModel, projectSFIDs []string) + SendEmailToOrgAdmin(ctx context.Context, input EmailToOrgAdminModel) + ContributorEmailToOrgAdmin(ctx context.Context, input ContributorEmailToOrgAdminModel) + SendEmailToCLAManagerDesigneeCorporate(ctx context.Context, input ToCLAManagerDesigneeCorporateModel) + SendEmailToCLAManagerDesignee(ctx context.Context, input ToCLAManagerDesigneeModel) + SendDesigneeEmailToUserWithNoLFID(ctx context.Context, input DesigneeEmailToUserWithNoLFIDModel) error + SendEmailToUserWithNoLFID(ctx context.Context, input EmailToUserWithNoLFIDModel) error } // NewService returns instance of CLA Manager service -func NewService(compService company.IService, projService project.Service, mgrService v1ClaManager.IService, claUserService easyCLAUser.Service, +func NewService(emailTemplateService emails.EmailTemplateService, compService company.IService, projService service2.Service, mgrService v1ClaManager.IService, claUserService easyCLAUser.Service, repoService repositories.Service, v2CompService v2Company.Service, evService events.Service, projectCGroupRepo projects_cla_groups.Repository) Service { return &service{ - companyService: compService, - projectService: projService, - repositoriesService: repoService, - managerService: mgrService, - easyCLAUserService: claUserService, - v2CompanyService: v2CompService, - eventService: evService, - projectCGRepo: projectCGroupRepo, + emailTemplateService: emailTemplateService, + companyService: compService, + projectService: projService, + repositoriesService: repoService, + managerService: mgrService, + easyCLAUserService: claUserService, + v2CompanyService: v2CompService, + eventService: evService, + projectCGRepo: projectCGroupRepo, } } // CreateCLAManager creates Cla Manager -func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, params cla_manager.CreateCLAManagerParams, authUsername string) (*models.CompanyClaManager, *models.ErrorResponse) { +func (s *service) CreateCLAManager(ctx context.Context, authUser *auth.User, claGroupID string, params cla_manager.CreateCLAManagerParams, authUsername string) (*models.CompanyClaManager, *models.ErrorResponse) { f := logrus.Fields{ - "functionName": "CreateCLAManager", + "functionName": "cla_manager.service.CreateCLAManager", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, + "companyID": params.CompanyID, "authUsername": authUsername, "xUserName": params.XUSERNAME, "xEmail": params.XEMAIL, } - // Search for salesForce Company aka external Company - log.WithFields(f).Debugf("Getting company by external ID : %s", params.CompanySFID) - companyModel, companyErr := s.companyService.GetCompanyByExternalID(ctx, params.CompanySFID) - if companyErr != nil || companyModel == nil { + // Search for Company by internal ID + log.WithFields(f).Debugf("Getting company by ID: %s", params.CompanyID) + v1CompanyModel, companyErr := s.companyService.GetCompany(ctx, params.CompanyID) + if companyErr != nil || v1CompanyModel == nil { msg := buildErrorMessage("company lookup error", claGroupID, params, companyErr) log.WithFields(f).Warn(msg) return nil, &models.ErrorResponse{ @@ -141,6 +145,8 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param Code: "400", } } + f["companySFID"] = v1CompanyModel.CompanyExternalID + f["companyName"] = v1CompanyModel.CompanyName claGroup, err := s.projectService.GetCLAGroupByID(ctx, claGroupID) if err != nil || claGroup == nil { @@ -151,6 +157,7 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param Code: "400", } } + // Get user by email userServiceClient := v2UserService.GetClient() // Get Manager lf account by username. Used for email content @@ -159,7 +166,7 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param msg := fmt.Sprintf("Failed to get Lfx User with username : %s ", authUsername) log.WithFields(f).Warn(msg) } - user, userErr := userServiceClient.SearchUserByEmail(params.Body.UserEmail.String()) + user, userErr := userServiceClient.SearchUsersByEmail(params.Body.UserEmail.String()) // Check for potential user with no username if user != nil && user.Username == "" { @@ -180,7 +187,7 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param } // Check if user exists in easyCLA DB, if not add User - log.WithFields(f).Debugf("Checking user: %+v in easyCLA records", user) + log.WithFields(f).Debugf("Checking user: %+v in EasyCLA database...", user) claUser, claUserErr := s.easyCLAUserService.GetUserByLFUserName(user.Username) if claUserErr != nil { msg := fmt.Sprintf("Problem getting claUser by :%s, error: %+v ", user.Username, claUserErr) @@ -197,8 +204,9 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param userName := fmt.Sprintf("%s %s", *params.Body.FirstName, *params.Body.LastName) _, currentTimeString := utils.CurrentTime() claUserModel := &v1Models.User{ - UserExternalID: params.CompanySFID, - LfEmail: *user.Emails[0].EmailAddress, + CompanyID: v1CompanyModel.CompanyID, + UserExternalID: v1CompanyModel.CompanyExternalID, + LfEmail: strfmt.Email(*user.Emails[0].EmailAddress), Admin: true, LfUsername: user.Username, DateCreated: currentTimeString, @@ -231,7 +239,7 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param } // Add CLA Manager to Database - signature, addErr := s.managerService.AddClaManager(ctx, companyModel.CompanyID, claGroupID, user.Username) + signature, addErr := s.managerService.AddClaManager(ctx, authUser, v1CompanyModel.CompanyID, claGroupID, user.Username, projectSF.Name) if addErr != nil { msg := buildErrorMessageCreate(params, addErr) log.WithFields(f).Warn(msg) @@ -241,7 +249,7 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param } } if signature == nil { - sigMsg := fmt.Sprintf("Signature not found for project: %s and company: %s ", claGroupID, companyModel.CompanyID) + sigMsg := fmt.Sprintf("Signature not found for project: %s and company: %s ", claGroupID, v1CompanyModel.CompanyID) log.WithFields(f).Warn(sigMsg) return nil, &models.ErrorResponse{ Message: sigMsg, @@ -258,27 +266,30 @@ func (s *service) CreateCLAManager(ctx context.Context, claGroupID string, param ClaGroupName: claGroup.ProjectName, ProjectID: claGroupID, ProjectName: projectSF.Name, - OrganizationName: companyModel.CompanyName, - OrganizationSfid: params.CompanySFID, + OrganizationName: v1CompanyModel.CompanyName, + OrganizationSfid: v1CompanyModel.CompanyExternalID, + OrganizationID: v1CompanyModel.CompanyID, Name: fmt.Sprintf("%s %s", user.FirstName, user.LastName), } + return claCompanyManager, nil } -func (s *service) DeleteCLAManager(ctx context.Context, claGroupID string, params cla_manager.DeleteCLAManagerParams) *models.ErrorResponse { +func (s *service) DeleteCLAManager(ctx context.Context, authUser *auth.User, claGroupID string, params cla_manager.DeleteCLAManagerParams) *models.ErrorResponse { f := logrus.Fields{ - "functionName": "DeleteCLAManager", + "functionName": "cla_manager.service.DeleteCLAManager", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, - "companySFID": params.CompanySFID, - "xUserName": params.XUSERNAME, - "xEmail": params.XEMAIL, + "companyID": params.CompanyID, + "authUserName": authUser.UserName, + "authUserEmail": authUser.Email, } - // Search for salesForce Company aka external Company - companyModel, companyErr := s.companyService.GetCompanyByExternalID(ctx, params.CompanySFID) - if companyErr != nil || companyModel == nil { - msg := buildErrorMessageDelete(params, companyErr) + // GetSFProject + ps := v2ProjectService.GetClient() + projectSF, deleteErr := ps.GetProject(params.ProjectSFID) + if deleteErr != nil { + msg := buildErrorMessageDelete(params, deleteErr) log.WithFields(f).Warn(msg) return &models.ErrorResponse{ Message: msg, @@ -286,7 +297,7 @@ func (s *service) DeleteCLAManager(ctx context.Context, claGroupID string, param } } - signature, deleteErr := s.managerService.RemoveClaManager(ctx, companyModel.CompanyID, claGroupID, params.UserLFID) + signature, deleteErr := s.managerService.RemoveClaManager(ctx, authUser, params.CompanyID, claGroupID, params.UserLFID, projectSF.Name) if deleteErr != nil { msg := buildErrorMessageDelete(params, deleteErr) @@ -296,8 +307,9 @@ func (s *service) DeleteCLAManager(ctx context.Context, claGroupID string, param Code: "400", } } + if signature == nil { - msg := fmt.Sprintf("Not found signature for project: %s and company: %s ", claGroupID, companyModel.CompanyID) + msg := fmt.Sprintf("CCLA signature not found for project: %s and company: %s ", claGroupID, params.CompanyID) log.WithFields(f).Warn(msg) return &models.ErrorResponse{ Message: msg, @@ -308,12 +320,12 @@ func (s *service) DeleteCLAManager(ctx context.Context, claGroupID string, param return nil } -//CreateCLAManagerDesignee creates designee for cla manager prospect -func (s *service) CreateCLAManagerDesignee(ctx context.Context, companySFID string, projectSFID string, userEmail string) (*models.ClaManagerDesignee, error) { +// CreateCLAManagerDesignee creates designee for cla manager prospect +func (s *service) CreateCLAManagerDesignee(ctx context.Context, companyID string, projectSFID string, userEmail string) (*models.ClaManagerDesignee, error) { f := logrus.Fields{ - "functionName": "CreateCLAManagerDesignee", + "functionName": "cla_manager.service.CreateCLAManagerDesignee", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "companySFID": companySFID, + "companyID": companyID, "projectSFID": projectSFID, "userEmail": userEmail, } @@ -323,12 +335,14 @@ func (s *service) CreateCLAManagerDesignee(ctx context.Context, companySFID stri orgClient := v2OrgService.GetClient() projectClient := v2ProjectService.GetClient() - log.WithFields(f).Debugf("loading company by external ID...") - v1CompanyModel, companyErr := s.companyService.GetCompanyByExternalID(ctx, companySFID) + log.WithFields(f).Debugf("loading company by ID...") + v1CompanyModel, companyErr := s.companyService.GetCompany(ctx, companyID) if companyErr != nil { log.WithFields(f).Warnf("company not found, error: %+v", companyErr) return nil, companyErr } + f["companySFID"] = v1CompanyModel.CompanyExternalID + f["companyName"] = v1CompanyModel.CompanyName log.WithFields(f).Debugf("checking if company/project is signed with CLA managers...") isSigned, signedErr := s.isSigned(ctx, v1CompanyModel, projectSFID) @@ -355,9 +369,11 @@ func (s *service) CreateCLAManagerDesignee(ctx context.Context, companySFID stri return nil, ErrLFXUserNotFound } + log.WithFields(f).Debugf("user found: %+v", lfxUser) + log.WithFields(f).Debugf("checking if user has %s role scope...", utils.CLADesigneeRole) // Check if user is already CLA Manager designee of project|organization scope - hasRoleScope, hasRoleScopeErr := orgClient.IsUserHaveRoleScope(ctx, utils.CLADesigneeRole, lfxUser.ID, companySFID, projectSFID) + hasRoleScope, hasRoleScopeErr := orgClient.IsUserHaveRoleScope(ctx, utils.CLADesigneeRole, lfxUser.ID, v1CompanyModel.CompanyExternalID, projectSFID) if hasRoleScopeErr != nil { // Skip 404 for ListOrgUsrServiceScopes endpoint if _, ok := hasRoleScopeErr.(*organizations.ListOrgUsrServiceScopesNotFound); !ok { @@ -385,31 +401,33 @@ func (s *service) CreateCLAManagerDesignee(ctx context.Context, companySFID stri } log.WithFields(f).Debugf("creating user role organization scope for user: %s, with role: %s with role ID: %s using project|org: %s|%s...", - userEmail, utils.CLADesigneeRole, roleID, projectSFID, companySFID) - scopeErr := orgClient.CreateOrgUserRoleOrgScopeProjectOrg(ctx, userEmail, projectSFID, companySFID, roleID) + userEmail, utils.CLADesigneeRole, roleID, projectSFID, v1CompanyModel.CompanyExternalID) + scopeErr := orgClient.CreateOrgUserRoleOrgScopeProjectOrg(ctx, userEmail, projectSFID, v1CompanyModel.CompanyExternalID, roleID) if scopeErr != nil { // Ignore conflict - role has already been assigned - otherwise, return error if _, ok := scopeErr.(*organizations.CreateOrgUsrRoleScopesConflict); !ok { - log.Warn(fmt.Sprintf("Problem creating projectOrg scope for email: %s , projectSFID: %s, companyID: %s", userEmail, projectSFID, companySFID)) + log.Warn(fmt.Sprintf("problem creating projectOrg scope for email: %s , projectSFID: %s, companySFID: %s", userEmail, projectSFID, v1CompanyModel.CompanyExternalID)) return nil, scopeErr } } log.WithFields(f).Debugf("created user role organization scope for user: %s, with role: %s with role ID: %s using project|org: %s|%s...", - userEmail, utils.CLADesigneeRole, roleID, projectSFID, companySFID) + userEmail, utils.CLADesigneeRole, roleID, projectSFID, v1CompanyModel.CompanyExternalID) // Log Event - s.eventService.LogEvent( + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ - EventType: events.AssignUserRoleScopeType, - LfUsername: lfxUser.Username, - ExternalProjectID: projectSFID, - CompanyModel: v1CompanyModel, - CompanyID: v1CompanyModel.CompanyID, - UserModel: &v1Models.User{LfUsername: lfxUser.Username, UserID: lfxUser.ID}, + EventType: events.AssignUserRoleScopeType, + ProjectSFID: projectSFID, + CompanyModel: v1CompanyModel, + CompanyID: v1CompanyModel.CompanyID, + UserName: lfxUser.Username, + LfUsername: lfxUser.Username, EventData: &events.AssignRoleScopeData{ - Role: "cla-manager-designee", - Scope: fmt.Sprintf("%s|%s", projectSFID, companySFID), + Role: "cla-manager-designee", + Scope: fmt.Sprintf("%s|%s", projectSFID, v1CompanyModel.CompanyExternalID), + UserName: lfxUser.Username, + UserEmail: userEmail, }, }) @@ -420,7 +438,8 @@ func (s *service) CreateCLAManagerDesignee(ctx context.Context, companySFID stri AssignedOn: time.Now().String(), Email: strfmt.Email(userEmail), ProjectSfid: projectSFID, - CompanySfid: companySFID, + CompanySfid: v1CompanyModel.CompanyExternalID, + CompanyID: v1CompanyModel.CompanyID, ProjectName: projectSF.Name, } @@ -428,11 +447,12 @@ func (s *service) CreateCLAManagerDesignee(ctx context.Context, companySFID stri } func (s *service) IsCLAManagerDesignee(ctx context.Context, companySFID, claGroupID, userLFID string) (*models.UserRoleStatus, error) { - f := logrus.Fields{ - "functionName": "IsCLAManagerDesignee", - "claGroupID": claGroupID, - "userLFID": userLFID, + "functionName": "cla_manager.service.IsCLAManagerDesignee", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companySFID": companySFID, + "claGroupID": claGroupID, + "userLFID": userLFID, } // Get LF User @@ -444,7 +464,7 @@ func (s *service) IsCLAManagerDesignee(ctx context.Context, companySFID, claGrou } log.WithFields(f).Debugf("Getting project sf mappings for claGroupID: %s ", claGroupID) - pcgs, pcgErr := s.projectCGRepo.GetProjectsIdsForClaGroup(claGroupID) + pcgs, pcgErr := s.projectCGRepo.GetProjectsIdsForClaGroup(ctx, claGroupID) if pcgErr != nil { log.WithFields(f).Warnf("Problem getting mappings for claGroup: %s , error: %+v ", claGroupID, pcgErr) return nil, pcgErr @@ -455,86 +475,74 @@ func (s *service) IsCLAManagerDesignee(ctx context.Context, companySFID, claGrou var hasRole = false if len(pcgs) > 0 { - - foundationSFID := pcgs[0].FoundationSFID - log.WithFields(f).Debugf("Check signed level status for foundationSFID: %s ...", foundationSFID) - signedAtFoundationLevel, signedErr := s.projectService.SignedAtFoundationLevel(ctx, foundationSFID) - if signedErr != nil { - log.WithFields(f).Warnf("problem checking for signed level for foundationSFID: %s ", foundationSFID) - return nil, signedErr + // Check for role at project level + log.WithFields(f).Debugf("Checking role for user: %s at project level for %d projects", user.ID, len(pcgs)) + type result struct { + hasRole bool + projectSFID string + err error } - if signedAtFoundationLevel { - // Check if user has cla-manager-designee role at foundation level - hasfoundationLevelRole, roleErr := orgClient.IsUserHaveRoleScope(ctx, utils.CLADesigneeRole, user.ID, companySFID, foundationSFID) - if roleErr != nil { - log.WithFields(f).Debugf("problem getting role:%s for user and project: %s ", utils.CLADesigneeRole, foundationSFID) - return nil, roleErr - } - hasRole = hasfoundationLevelRole - } else { - // Check for role at project level - type result struct { - hasRole bool - err error - } - roleStatusChan := make(chan *result) - var wg sync.WaitGroup - wg.Add(len(pcgs)) - - go func() { - wg.Wait() - close(roleStatusChan) - }() - - for _, pcg := range pcgs { - go func(swg *sync.WaitGroup, pcg *projects_cla_groups.ProjectClaGroup, roleStatusChan chan *result) { - defer swg.Done() - var output result - log.WithFields(f).Debugf("Checking role status for projectSFID: %s", pcg.ProjectSFID) - hasProjectLevelRole, roleErr := orgClient.IsUserHaveRoleScope(ctx, utils.CLADesigneeRole, user.ID, companySFID, pcg.ProjectSFID) - if roleErr != nil { - log.WithFields(f).Debugf("problem getting role:%s for user and project: %s ", utils.CLADesigneeRole, pcg.ProjectSFID) - output = result{ - hasRole: false, - err: roleErr, - } - roleStatusChan <- &output - return + roleStatusChan := make(chan *result) + var wg sync.WaitGroup + wg.Add(len(pcgs)) + + go func() { + wg.Wait() + close(roleStatusChan) + }() + + for _, pcg := range pcgs { + go func(swg *sync.WaitGroup, pcg *projects_cla_groups.ProjectClaGroup, roleStatusChan chan *result) { + defer swg.Done() + var output result + log.WithFields(f).Debugf("Checking role status for projectSFID: %s", pcg.ProjectSFID) + hasProjectLevelRole, roleErr := orgClient.IsUserHaveRoleScope(ctx, utils.CLADesigneeRole, user.ID, companySFID, pcg.ProjectSFID) + if roleErr != nil { + log.WithFields(f).Debugf("problem getting role:%s for user and project: %s ", utils.CLADesigneeRole, pcg.ProjectSFID) + output = result{ + hasRole: false, + err: roleErr, + projectSFID: pcg.ProjectSFID, + } + roleStatusChan <- &output + return + } + if hasProjectLevelRole { + log.WithFields(f).Debugf("user has :%s role for company: %s ", utils.CLADesigneeRole, companySFID) + roleStatusChan <- &result{ + hasRole: true, + err: nil, + projectSFID: pcg.ProjectSFID, } - if hasProjectLevelRole { - log.WithFields(f).Debugf("user has :%s role for company: %s ", utils.CLADesigneeRole, companySFID) - roleStatusChan <- &result{ - hasRole: true, - err: nil, - } - } else { - log.WithFields(f).Debugf("user does not have :%s role for company: %s ", utils.CLADesigneeRole, companySFID) - roleStatusChan <- &result{ - hasRole: false, - err: nil, - } + } else { + log.WithFields(f).Debugf("user does not have :%s role for company: %s ", utils.CLADesigneeRole, companySFID) + roleStatusChan <- &result{ + hasRole: false, + err: nil, + projectSFID: pcg.ProjectSFID, } + } + + }(&wg, pcg, roleStatusChan) + } - }(&wg, pcg, roleStatusChan) + //confirm user has cla-manager-designee for any of the projects + for resultCh := range roleStatusChan { + if resultCh.err != nil { + return nil, resultCh.err } - //confirm user has cla-manager-designee for all projects - for resultCh := range roleStatusChan { - if resultCh.err != nil { - return nil, resultCh.err - } - if !resultCh.hasRole { - log.WithFields(f).Debugf("User %s does not have role: %s at project level", userLFID, utils.CLADesigneeRole) - hasRole = false - return &models.UserRoleStatus{ - HasRole: &hasRole, - LfUsername: userLFID, - }, nil - } + if resultCh.hasRole { + log.WithFields(f).Debugf("User %s has %s role for project : %s", userLFID, utils.CLADesigneeRole, resultCh.projectSFID) + hasRole = true + return &models.UserRoleStatus{ + HasRole: &hasRole, + LfUsername: userLFID, + }, nil } - log.WithFields(f).Debugf("User %s has %s role at project level", userLFID, utils.CLADesigneeRole) - hasRole = true } + log.WithFields(f).Debugf("User %s has %s role at project level", userLFID, utils.CLADesigneeRole) + hasRole = true } @@ -544,97 +552,94 @@ func (s *service) IsCLAManagerDesignee(ctx context.Context, companySFID, claGrou }, nil } -//CreateCLAManagerDesigneeByGroup creates designee by group for cla manager prospect -func (s *service) CreateCLAManagerDesigneeByGroup(ctx context.Context, params cla_manager.CreateCLAManagerDesigneeByGroupParams, projectCLAGroups []*projects_cla_groups.ProjectClaGroup, f logrus.Fields) ([]*models.ClaManagerDesignee, string, error) { +// CreateCLAManagerDesigneeByGroup creates designee by group for cla manager prospect +func (s *service) CreateCLAManagerDesigneeByGroup(ctx context.Context, params cla_manager.CreateCLAManagerDesigneeByGroupParams, projectCLAGroups []*projects_cla_groups.ProjectClaGroup) ([]*models.ClaManagerDesignee, string, error) { + f := logrus.Fields{ + "functionName": "cla_manager.service.CreateCLAManagerDesigneeByGroup", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": params.ClaGroupID, + "companyID": params.CompanyID, + "userEmail": params.Body.UserEmail.String(), + } + var designeeScopes []*models.ClaManagerDesignee userEmail := params.Body.UserEmail.String() - foundationSFID := projectCLAGroups[0].FoundationSFID - signedAtFoundationLevel, signedErr := s.projectService.SignedAtFoundationLevel(ctx, foundationSFID) - if signedErr != nil { - msg := fmt.Sprintf("Problem getting level of CLA Group Signature for claGroup: %s ", foundationSFID) - return nil, msg, signedErr + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + v1CompanyModel, err := s.companyService.GetCompany(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) + return nil, msg, err } + f["companySFID"] = v1CompanyModel.CompanyExternalID + f["companyName"] = v1CompanyModel.CompanyName - if signedAtFoundationLevel { - if foundationSFID != "" { - claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, params.CompanySFID, foundationSFID, userEmail) - if err != nil { - if err == ErrCLAManagerDesigneeConflict { - msg := fmt.Sprintf("Conflict assigning cla manager role for Foundation SFID: %s ", foundationSFID) - return nil, msg, err - } - msg := fmt.Sprintf("Creating cla manager failed for Foundation SFID: %s ", foundationSFID) - return nil, msg, err - } - designeeScopes = append(designeeScopes, claManagerDesignee) - } - } else { - // Channel result - type result struct { - designee *models.ClaManagerDesignee - msg string - err error - } - designeeChan := make(chan *result) - var wg sync.WaitGroup - wg.Add(len(projectCLAGroups)) + // Channel result + type result struct { + designee *models.ClaManagerDesignee + msg string + err error + } + designeeChan := make(chan *result) + var wg sync.WaitGroup + wg.Add(len(projectCLAGroups)) - go func() { - wg.Wait() - close(designeeChan) - }() + go func() { + wg.Wait() + close(designeeChan) + }() - for _, pcg := range projectCLAGroups { - go func(swg *sync.WaitGroup, pcg *projects_cla_groups.ProjectClaGroup, designeeChannel chan *result) { - defer swg.Done() - log.WithFields(f).Debugf("creating CLA Manager Designee for Project SFID: %s", pcg.ProjectSFID) - claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, params.CompanySFID, pcg.ProjectSFID, userEmail) - var output result - if err != nil { - if err == ErrCLAManagerDesigneeConflict { - msg := fmt.Sprintf("Conflict assigning cla manager role for Project SFID: %s, error: %s ", pcg.ProjectSFID, err) - output = result{ - designee: nil, - msg: msg, - err: ErrCLAManagerDesigneeConflict, - } - } - msg := fmt.Sprintf("Creating cla manager failed for Project SFID: %s, error: %s ", pcg.ProjectSFID, err) + for _, pcg := range projectCLAGroups { + go func(swg *sync.WaitGroup, pcg *projects_cla_groups.ProjectClaGroup, designeeChannel chan *result) { + defer swg.Done() + log.WithFields(f).Debugf("creating CLA Manager Designee for Project SFID: %s", pcg.ProjectSFID) + claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, v1CompanyModel.CompanyID, pcg.ProjectSFID, userEmail) + var output result + if err != nil { + if err == ErrCLAManagerDesigneeConflict { + msg := fmt.Sprintf("Conflict assigning cla manager role for Project SFID: %s, error: %s ", pcg.ProjectSFID, err) output = result{ designee: nil, msg: msg, - err: err, + err: ErrCLAManagerDesigneeConflict, } - designeeChannel <- &output - return } + msg := fmt.Sprintf("Creating cla manager failed for Project SFID: %s, error: %s ", pcg.ProjectSFID, err) output = result{ - designee: claManagerDesignee, - msg: "", - err: nil, + designee: nil, + msg: msg, + err: err, } designeeChannel <- &output - }(&wg, pcg, designeeChan) - } - for resultCh := range designeeChan { - if resultCh.err != nil { - return nil, resultCh.msg, resultCh.err + return } - designeeScopes = append(designeeScopes, resultCh.designee) + output = result{ + designee: claManagerDesignee, + msg: "", + err: nil, + } + designeeChannel <- &output + }(&wg, pcg, designeeChan) + } + for resultCh := range designeeChan { + if resultCh.err != nil { + return nil, resultCh.msg, resultCh.err } + designeeScopes = append(designeeScopes, resultCh.designee) } return designeeScopes, "", nil } // CreateCLAManagerRequest service method -func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool, companySFID string, projectID string, userEmail string, fullName string, authUser *auth.User, LfxPortalURL string) (*models.ClaManagerDesignee, error) { +func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, fullName string, authUser *auth.User) (*models.ClaManagerDesignee, error) { f := logrus.Fields{ - "functionName": "CreateCLAManagerRequest", + "functionName": "cla_manager.service.CreateCLAManagerRequest", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "contactAdmin": contactAdmin, - "companySFID": companySFID, + "companyID": companyID, "projectID": projectID, "userEmail": userEmail, "fullName": fullName, @@ -643,15 +648,18 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool } orgService := v2OrgService.GetClient() + userService := v2UserService.GetClient() log.WithFields(f).Debugf("loading company by external ID...") // Search for salesForce Company aka external Company - v1CompanyModel, companyErr := s.companyService.GetCompanyByExternalID(ctx, companySFID) + v1CompanyModel, companyErr := s.companyService.GetCompany(ctx, companyID) if companyErr != nil { msg := fmt.Sprintf("EasyCLA - 400 Bad Request - %s", companyErr) log.Warn(msg) return nil, companyErr } + f["companyID"] = v1CompanyModel.CompanyID + f["companyName"] = v1CompanyModel.CompanyName // Determine if the CCLA is already signed or not log.WithFields(f).Debugf("checking if company/project is signed with CLA managers...") @@ -683,33 +691,49 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool if contactAdmin { log.WithFields(f).Debug("sending email to company Admin") log.WithFields(f).Debug("querying user admin scopes...") - scopes, listScopeErr := orgService.ListOrgUserAdminScopes(ctx, companySFID, nil) + scopes, listScopeErr := orgService.ListOrgUserAdminScopes(ctx, v1CompanyModel.CompanyExternalID, nil) if listScopeErr != nil { msg := fmt.Sprintf("EasyCLA - 400 Bad Request - Admin lookup error for organisation SFID: %s, error: %+v ", - companySFID, listScopeErr) + v1CompanyModel.CompanyExternalID, listScopeErr) log.WithFields(f).Warn(msg) return nil, listScopeErr } if len(scopes.Userroles) == 0 { msg := fmt.Sprintf("EasyCLA - 404 NotFound - No admins for organization SFID: %s", - companySFID) + v1CompanyModel.CompanyExternalID) log.WithFields(f).Warn(msg) return nil, ErrNoOrgAdmins } for _, admin := range scopes.Userroles { log.WithFields(f).Debugf("sending email to organization admin: %+v", admin) - sendEmailToOrgAdmin(admin.Contact.EmailAddress, admin.Contact.Name, v1CompanyModel.CompanyName, []string{projectSF.Name}, authUser.Email, authUser.UserName, LfxPortalURL) + + adminUser, adminErr := userService.GetUser(admin.Contact.ID) + if adminErr != nil { + msg := fmt.Sprintf("Failed to get user for ID: %s ", admin.Contact.ID) + log.Warn(msg) + return nil, adminErr + } + s.SendEmailToOrgAdmin(ctx, + EmailToOrgAdminModel{ + adminEmail: userService.GetPrimaryEmail(adminUser), + adminName: admin.Contact.Name, + companyName: v1CompanyModel.CompanyName, + projectName: projectSF.Name, + projectSFID: projectSF.ID, + senderName: authUser.UserName, + senderEmail: authUser.Email, + }) // Make a note in the event log - s.eventService.LogEvent(&events.LogEventArgs{ - EventType: events.ContributorNotifyCompanyAdminType, - LfUsername: authUser.UserName, - ExternalProjectID: projectID, - CompanyID: v1CompanyModel.CompanyID, + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ContributorNotifyCompanyAdminType, + LfUsername: authUser.UserName, + ProjectSFID: projectID, + CompanyID: v1CompanyModel.CompanyID, EventData: &events.ContributorNotifyCompanyAdminData{ AdminName: admin.Contact.Name, - AdminEmail: admin.Contact.EmailAddress, + AdminEmail: userService.GetPrimaryEmail(adminUser), }, }) } @@ -718,7 +742,6 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool } log.WithFields(f).Debug("not sending admin email...") - userService := v2UserService.GetClient() log.WithFields(f).Debug("searching user in user service...") // This routine is taking 24-29 seconds when running locally -> User service in DEV //lfxUser, userErr := userService.SearchUserByEmail(userEmail) @@ -728,7 +751,17 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool msg := fmt.Sprintf("User: %s does not have an LF Login", userEmail) log.WithFields(f).Warn(msg) // Send email - sendEmailErr := sendEmailToUserWithNoLFID(ctx, projectSF.Name, authUser.UserName, authUser.Email, fullName, userEmail, companySFID, &projectSF.ID, utils.CLADesigneeRole) + sendEmailErr := s.SendEmailToUserWithNoLFID(ctx, EmailToUserWithNoLFIDModel{ + projectName: projectSF.Name, + requesterUsername: authUser.UserName, + requesterEmail: authUser.Email, + userWithNoLFIDName: fullName, + userWithNoLFIDEmail: userEmail, + organizationID: v1CompanyModel.CompanyExternalID, + companyName: v1CompanyModel.CompanyName, + projectID: projectSF.ID, + role: utils.CLADesigneeRole, + }) if sendEmailErr != nil { log.WithFields(f).Warnf("Error sending email: %+v", sendEmailErr) return nil, sendEmailErr @@ -741,7 +774,7 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool } log.WithFields(f).Debug("sending CLA manager designee request...") - claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, companySFID, projectID, userEmail) + claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, companyID, projectID, userEmail) if err != nil { // Check conflict for role scope if _, ok := err.(*organizations.CreateOrgUsrRoleScopesConflict); ok { @@ -754,11 +787,11 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool log.WithFields(f).Debug("creating a contributor assigned CLA designee log event...") // Make a note in the event log - s.eventService.LogEvent(&events.LogEventArgs{ - EventType: events.ContributorAssignCLADesigneeType, - LfUsername: authUser.UserName, - ExternalProjectID: projectID, - CompanyID: v1CompanyModel.CompanyID, + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ContributorAssignCLADesigneeType, + LfUsername: authUser.UserName, + ProjectSFID: projectID, + CompanyID: v1CompanyModel.CompanyID, EventData: &events.ContributorAssignCLADesignee{ DesigneeName: claManagerDesignee.LfUsername, DesigneeEmail: claManagerDesignee.Email.String(), @@ -767,15 +800,23 @@ func (s *service) CreateCLAManagerRequest(ctx context.Context, contactAdmin bool log.WithFields(f).Debugf("sending Email to CLA Manager Designee email: %s ", userEmail) designeeName := fmt.Sprintf("%s %s", lfxUser.FirstName, lfxUser.LastName) - sendEmailToCLAManagerDesigneeCorporate(ctx, LfxPortalURL, v1CompanyModel.CompanyName, []string{projectSF.Name}, userEmail, designeeName, authUser.Email, authUser.UserName) + s.SendEmailToCLAManagerDesigneeCorporate(ctx, ToCLAManagerDesigneeCorporateModel{ + companyName: v1CompanyModel.CompanyName, + projectSFID: projectSF.ID, + projectName: projectSF.Name, + designeeEmail: userEmail, + designeeName: designeeName, + senderEmail: authUser.Email, + senderName: authUser.UserName, + }) log.WithFields(f).Debug("creating a contributor notify CLA designee log event...") // Make a note in the event log - s.eventService.LogEvent(&events.LogEventArgs{ - EventType: events.ContributorNotifyCLADesigneeType, - LfUsername: authUser.UserName, - ExternalProjectID: projectID, - CompanyID: v1CompanyModel.CompanyID, + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.ContributorNotifyCLADesigneeType, + LfUsername: authUser.UserName, + ProjectSFID: projectID, + CompanyID: v1CompanyModel.CompanyID, EventData: &events.ContributorNotifyCLADesignee{ DesigneeName: claManagerDesignee.LfUsername, DesigneeEmail: claManagerDesignee.Email.String(), @@ -802,7 +843,7 @@ func (s *service) ValidateInviteCompanyAdminCheck(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 } @@ -812,17 +853,19 @@ func (s *service) ValidateInviteCompanyAdminCheck(ctx context.Context, f logrus. return nil } -func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, name string, contributor *v1User.User, LfxPortalURL string) ([]*models.ClaManagerDesignee, error) { - orgService := v2OrgService.GetClient() - projectService := v2ProjectService.GetClient() - userService := v2UserService.GetClient() +func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, companyID string, projectID string, userEmail string, name string, contributor *v1User.User) ([]*models.ClaManagerDesignee, error) { //nolint f := logrus.Fields{ - "functionName": "InviteCompanyAdmin", + "functionName": "cla_manager.service.InviteCompanyAdmin", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "companyID": companyID, "claGroupID": projectID, "userEmail": userEmail, - "name": name} + "name": name, + } + + orgService := v2OrgService.GetClient() + projectService := v2ProjectService.GetClient() + userService := v2UserService.GetClient() validateError := s.ValidateInviteCompanyAdminCheck(ctx, f, projectID, contactAdmin, userEmail, name, contributor) if validateError != nil { @@ -831,7 +874,7 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com // Get project cla Group records log.WithFields(f).Debugf("Getting SalesForce Projects for claGroup: %s ", projectID) - projectCLAGroups, getErr := s.projectCGRepo.GetProjectsIdsForClaGroup(projectID) + projectCLAGroups, getErr := s.projectCGRepo.GetProjectsIdsForClaGroup(ctx, projectID) if getErr != nil { msg := fmt.Sprintf("Error getting SF projects for claGroup: %s ", projectID) log.Debug(msg) @@ -874,15 +917,35 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com } var projectSFs []string - for _, pcg := range projectCLAGroups { - log.WithFields(f).Debugf("Getting salesforce project by SFID: %s ", pcg.ProjectSFID) - projectSF, projectErr := projectService.GetProject(pcg.ProjectSFID) + var projectSFIDs []string + foundationSFID := projectCLAGroups[0].FoundationSFID + + if signedAtFoundation { + + // Get salesforce project by FoundationID + log.WithFields(f).Debugf("querying project service for project details...") + // GetSFProject + foundationSF, projectErr := projectService.GetProject(foundationSFID) if projectErr != nil { - msg := fmt.Sprintf("Problem getting salesforce Project ID: %s", pcg.ProjectSFID) + msg := fmt.Sprintf("EasyCLA - 400 Bad Request - Project service lookup error for SFID: %s, error : %+v", + projectID, projectErr) log.WithFields(f).Warn(msg) return nil, projectErr } - projectSFs = append(projectSFs, projectSF.Name) + projectSFs = append(projectSFs, foundationSF.Name) + projectSFIDs = append(projectSFIDs, foundationSFID) + } else { + for _, pcg := range projectCLAGroups { + log.WithFields(f).Debugf("Getting salesforce project by SFID: %s ", pcg.ProjectSFID) + projectSF, projectErr := projectService.GetProject(pcg.ProjectSFID) + if projectErr != nil { + msg := fmt.Sprintf("Problem getting salesforce Project ID: %s", pcg.ProjectSFID) + log.WithFields(f).Warn(msg) + return nil, projectErr + } + projectSFs = append(projectSFs, projectSF.Name) + projectSFIDs = append(projectSFIDs, projectSF.ID) + } } var designeeScopes []*models.ClaManagerDesignee @@ -906,8 +969,24 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com } for _, admin := range scopes.Userroles { - // Check if is Gerrit User or GH User - contributorEmailToOrgAdmin(admin.Contact.EmailAddress, admin.Contact.Name, organization.Name, projectSFs, userModel, LfxPortalURL) + // Email details are masked so an extra query to get user details is used + log.WithFields(f).Debugf("Getting email for user with ID: %s ", admin.Contact.ID) + + adminUser, adminErr := userService.GetUser(admin.Contact.ID) + if adminErr != nil { + msg := fmt.Sprintf("Failed to get user for ID: %s ", admin.Contact.ID) + log.Warn(msg) + return nil, adminErr + } + + s.ContributorEmailToOrgAdmin(ctx, ContributorEmailToOrgAdminModel{ + adminEmail: userService.GetPrimaryEmail(adminUser), + adminName: admin.Contact.Name, + companyName: organization.Name, + projectSFIDs: projectSFIDs, + contributor: userModel, + userDetails: getFormattedUserDetails(userModel), + }) designeeScope := models.ClaManagerDesignee{ Email: strfmt.Email(admin.Contact.EmailAddress), Name: admin.Contact.Name, @@ -917,33 +996,39 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com return designeeScopes, nil } - signedError := s.ProjectComapnySignnedOrNot(ctx, f, signedAtFoundation, projectCLAGroups, companyModel) + signedError := s.ProjectCompanySignedOrNot(ctx, signedAtFoundation, projectCLAGroups, companyModel) if signedError != nil { return nil, signedError } // Get suggested CLA Manager user details - user, userErr := userService.SearchUserByEmail(userEmail) + user, userErr := userService.SearchUsersByEmail(userEmail) if userErr != nil || (user != nil && user.Username == "") { + var contributorModel emails.Contributor msg := fmt.Sprintf("UserEmail: %s has no LF Login and has been sent an invite email to create an account , error: %+v", userEmail, userErr) log.Warn(msg) - // Use FoundationSFID - foundationSFID := projectCLAGroups[0].FoundationSFID - - // Get salesforce project by FoundationID - log.WithFields(f).Debugf("querying project service for project details...") - // GetSFProject - ps := v2ProjectService.GetClient() - sfProject, projectErr := ps.GetProject(foundationSFID) - if projectErr != nil { - msg := fmt.Sprintf("EasyCLA - 400 Bad Request - Project service lookup error for SFID: %s, error : %+v", - projectID, projectErr) - log.WithFields(f).Warn(msg) - return nil, projectErr + // Get username and useremail details for contributor + if contributor.LFEmail != "" && contributor.UserName != "" { + contributorModel.Email = contributor.LFEmail + contributorModel.Username = contributor.LFUsername + contributorModel.EmailLabel = utils.EmailLabel + contributorModel.UsernameLabel = utils.UserLabel + } else { + contributorModel = getContributorPublicEmail(contributor) } - sendErr := sendDesigneeEmailToUserWithNoLFID(ctx, contributor.UserName, contributor.UserEmails[0], name, userEmail, organization.Name, organization.ID, sfProject.Name, &foundationSFID, "cla-manager-designee") + sendErr := s.SendDesigneeEmailToUserWithNoLFID(ctx, DesigneeEmailToUserWithNoLFIDModel{ + userWithNoLFIDName: name, + userWithNoLFIDEmail: userEmail, + contributorModel: contributorModel, + projectNames: projectSFs, + projectSFIDs: projectSFIDs, + foundationSFID: foundationSFID, + role: "cla-manager-designee", + companyName: organization.Name, + organizationID: organization.ID, + }) if sendErr != nil { msg := fmt.Sprintf("Problem sending email to user: %s , error: %+v", userEmail, sendErr) log.Warn(msg) @@ -956,7 +1041,7 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com // check if claGroup is signed at foundation level foundationSFID := projectCLAGroups[0].FoundationSFID log.WithFields(f).Debugf("Create cla manager designee for foundation : %s ", foundationSFID) - claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, organization.ID, foundationSFID, userEmail) + claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, companyID, foundationSFID, userEmail) if err != nil { msg := fmt.Sprintf("Problem creating cla Manager Designee for user : %s, error: %+v ", userEmail, err) log.WithFields(f).Warn(msg) @@ -966,7 +1051,7 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com } else { for _, pcg := range projectCLAGroups { log.WithFields(f).Debugf("Create cla manager designee for Project SFID: %s", pcg.ProjectSFID) - claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, organization.ID, pcg.ProjectSFID, userEmail) + claManagerDesignee, err := s.CreateCLAManagerDesignee(ctx, companyID, pcg.ProjectSFID, userEmail) if err != nil { msg := fmt.Sprintf("Problem creating cla Manager Designee for user : %s, error: %+v ", userEmail, err) log.WithFields(f).Warn(msg) @@ -975,18 +1060,34 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com designeeScopes = append(designeeScopes, claManagerDesignee) } } - conversionErr := s.convertGHUserToContact(ctx, contributor) - if conversionErr != nil { - return nil, conversionErr - } log.Debugf("Sending Email to CLA Manager Designee email: %s ", userEmail) + var contributorModel emails.Contributor + if contributor.LFUsername != "" && contributor.LFEmail != "" && len(projectSFs) > 0 { - sendEmailToCLAManagerDesignee(ctx, LfxPortalURL, organization.Name, projectSFs, userEmail, user.Name, contributor.LFEmail, contributor.LFUsername) + contributorModel.Email = contributor.LFEmail + contributorModel.Username = contributor.LFUsername + contributorModel.EmailLabel = utils.EmailLabel + contributorModel.UsernameLabel = utils.UserLabel + s.SendEmailToCLAManagerDesignee(ctx, ToCLAManagerDesigneeModel{ + designeeName: user.Name, + designeeEmail: userEmail, + companyName: organization.Name, + projectNames: projectSFs, + projectSFIDs: projectSFIDs, + contributorModel: contributorModel, + }) } else { - contributorUserName, contributorEmail := getContributorPublicEmail(contributor) - sendEmailToCLAManagerDesignee(ctx, LfxPortalURL, organization.Name, projectSFs, userEmail, user.Name, contributorUserName, contributorEmail) + contributorModel = getContributorPublicEmail(contributor) + s.SendEmailToCLAManagerDesignee(ctx, ToCLAManagerDesigneeModel{ + designeeName: user.Name, + designeeEmail: userEmail, + companyName: organization.Name, + projectNames: projectSFs, + projectSFIDs: projectSFIDs, + contributorModel: contributorModel, + }) } log.Debugf("CLA Manager designee created : %+v", designeeScopes) @@ -994,7 +1095,17 @@ func (s *service) InviteCompanyAdmin(ctx context.Context, contactAdmin bool, com return designeeScopes, nil } -func (s *service) ProjectComapnySignnedOrNot(ctx context.Context, f logrus.Fields, signedAtFoundation bool, projectCLAGroups []*projects_cla_groups.ProjectClaGroup, companyModel *v1Models.Company) error { + +func (s *service) ProjectCompanySignedOrNot(ctx context.Context, signedAtFoundation bool, projectCLAGroups []*projects_cla_groups.ProjectClaGroup, companyModel *v1Models.Company) error { + f := logrus.Fields{ + "functionName": "cla_manager.service.ProjectCompanySignedOrNot", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signedAtFoundation": signedAtFoundation, + "companyID": companyModel.CompanyID, + "companySFID": companyModel.CompanyExternalID, + "companyName": companyModel.CompanyName, + } + if signedAtFoundation { foundationSFID := projectCLAGroups[0].FoundationSFID @@ -1044,45 +1155,48 @@ func validateInviteCompanyAdmin(contactAdmin bool, userEmail string, name string return nil } -func (s *service) NotifyCLAManagers(ctx context.Context, notifyCLAManagers *models.NotifyClaManagerList, LfxPortalURL string) error { +func (s *service) NotifyCLAManagers(ctx context.Context, notifyCLAManagers *models.NotifyClaManagerList, CorporateConsoleV2URL string) error { + f := logrus.Fields{ + "functionName": "cla_manager.service.NotifyCLAManagers", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyName": notifyCLAManagers.CompanyName, + "signingEntityName": notifyCLAManagers.SigningEntityName, + "userID": notifyCLAManagers.UserID, + "claGroupName": notifyCLAManagers.ClaGroupID, + } // Search for Easy CLA User - log.Debugf("Getting user by ID: %s", notifyCLAManagers.UserID) + log.WithFields(f).Debugf("Getting user by ID: %s", notifyCLAManagers.UserID) userModel, userErr := s.easyCLAUserService.GetUser(notifyCLAManagers.UserID) if userErr != nil { msg := fmt.Sprintf("Problem getting user by ID: %s ", notifyCLAManagers.UserID) - log.Warn(msg) + log.WithFields(f).Warn(msg) return ErrCLAUserNotFound } - log.Debugf("Sending notification emails to claManagers: %+v", notifyCLAManagers.List) - for _, claManager := range notifyCLAManagers.List { - sendEmailToCLAManager(claManager.Name, claManager.Email.String(), userModel, notifyCLAManagers.CompanyName, notifyCLAManagers.ClaGroupName, LfxPortalURL) + // Get mappings + var projectSFIDs []string + pcgs, pcgErr := s.projectCGRepo.GetProjectsIdsForClaGroup(ctx, notifyCLAManagers.ClaGroupID) + if pcgErr != nil { + log.WithFields(f).Warnf("problem getting cla_group_mappings by claGroupID: %s ", notifyCLAManagers.ClaGroupID) + return pcgErr } - return nil -} + for _, pcg := range pcgs { + projectSFIDs = append(projectSFIDs, pcg.ProjectSFID) + } -func sendEmailToCLAManager(manager string, managerEmail string, userModel *v1Models.User, company string, claGroupName string, lfxPortalURL string) { - subject := fmt.Sprintf("EasyCLA: Approval Request for contributor: %s", getBestUserName(userModel)) - recipients := []string{managerEmail} - body := fmt.Sprintf(` -

    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("
      ") - for _, project := range projectNames { - sb.WriteString(fmt.Sprintf("
    • %s
    • ", project)) - } - sb.WriteString("
    ") - return sb.String() -} - -func sendEmailToOrgAdmin(adminEmail string, admin string, company string, projectNames []string, senderEmail string, senderName string, corporateConsole string) { - subject := fmt.Sprintf("EasyCLA: Invitation to Sign the %s Corporate CLA ", company) - recipients := []string{adminEmail} - 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`, - 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

    -

    Accept Invite

    - %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.

    -

    Accept Invite

    -%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(` + CLA Signed
    `, badgeHyperlink) + failedBadge := fmt.Sprintf(` +CLA Not Signed
    `, badgeHyperlink) + // missingUserIDBadge := fmt.Sprintf(` + // CLA Missing ID
    `, badgeHyperlink) + confirmationNeededBadge := fmt.Sprintf(` +CLA Confirmation Needed
    `, badgeHyperlink) + + var body string + + var result string + failed := ":x:" + success := ":white_check_mark:" + + if len(signedUsers) > 0 { + result = "
      " + for _, signed := range signedUsers { + authorInfo := getAuthorInfo(signed) + result += fmt.Sprintf("
    • %s %s
    • ", success, authorInfo) + } + result += "
    " + body = coveredBadge + } + + // gitlabSupportURL := "https://about.gitlab.com/support" + easyCLASupportURL := "https://jira.linuxfoundation.org/servicedesk/customer/portal/4" + // faq := "https://docs.linuxfoundation.org/lfx/easycla/v2-current/getting-started/easycla-troubleshooting#github-unable-to-contribute-to-easycla-enforced-repositories" + + if len(missingUsers) > 0 { + result += "
      " + for _, missingUser := range missingUsers { + authorInfo := getAuthorInfo(missingUser.User) + if errors.Is(missingUser.err, missingCompanyAffiliation) { + msg := fmt.Sprintf(`
    • %s %s. This user is authorized, but they must confirm their affiliation with their company. + Start the authorization process by clicking here, click "Corporate", + select the appropriate company from the list, then confirm your affiliation on the page that appears. + For further assistance with EasyCLA, + please submit a support request ticket.
    • `, failed, authorInfo, signURL, easyCLASupportURL) + result += msg + body = confirmationNeededBadge + } else { + msg := fmt.Sprintf(`
    • %s - %s. The commit is not authorized under a signed CLA. + Please click here to be authorized. + For further assistance with EasyCLA, + please submit a support request ticket. +
    • `, signURL, failed, authorInfo, signURL, easyCLASupportURL) + result += msg + body = failedBadge + } + } + result += "
    " + } + + if result != "" { + body += "

    " + result + } + + return body +} + +func GetFullSignURL(gitlabOrganizationID string, gitlabRepositoryID string, mrID string) string { + return fmt.Sprintf("%s/v4/repository-provider/%s/sign/%s/%s/%s/#/", + config.GetConfig().ClaAPIV4Base, + utils.GitLabLower, + gitlabOrganizationID, + gitlabRepositoryID, + mrID, + ) +} + +func getAuthorInfo(gitlabUser *gitlab.User) string { + f := logrus.Fields{ + "functionName": "getAuthorInfo", + "gitlabUsername": gitlabUser.Username, + "gitlabName": gitlabUser.Name, + "gitlabEmail": gitlabUser.Email, + } + log.WithFields(f).Debug("getting author info") + if gitlabUser.Username != "" { + return fmt.Sprintf("login:@%s/name:%s", gitlabUser.Username, gitlabUser.Name) + } else if gitlabUser.Email != "" { + return fmt.Sprintf("email:%s/name:%s", gitlabUser.Email, gitlabUser.Name) + } + return fmt.Sprintf("name:%s", gitlabUser.Name) +} + +func (s *service) getGitlabOrganizationFromProjectPath(ctx context.Context, projectPath, projectNameSpace string) (*v2Models.GitlabOrganization, error) { + parts := strings.Split(projectPath, "/") + organizationName := parts[0] + f := logrus.Fields{ + "functionName": "getGitlabOrganizationFromProjectPath", + "projectPath": projectPath, + "projectNameSpace": projectNameSpace, + "organizationName": organizationName, + } + + log.WithFields(f).Debug("getting gitlab org from project path") + gitlabOrg, err := s.gitlabOrgService.GetGitLabOrganizationByFullPath(ctx, organizationName) + if err != nil || gitlabOrg == nil { + // try getting it with project name as well + log.WithFields(f).Debugf("getting gitlab org with project name : %s", projectNameSpace) + gitlabOrg, err = s.gitlabOrgService.GetGitLabOrganizationByFullPath(ctx, projectNameSpace) + if err != nil || gitlabOrg == nil { + return nil, fmt.Errorf("gitlab org : %s doesn't exist : %v", organizationName, err) + } + } + + gitlabOrg, err = s.gitlabOrgService.GetGitLabOrganization(ctx, gitlabOrg.OrganizationID) + if err != nil { + return nil, fmt.Errorf("fetching gitlab org : %s failed : %v", gitlabOrg.OrganizationID, err) + } + + return gitlabOrg, nil +} + +func (s *service) getGitlabRepoByName(ctx context.Context, repoNameWithPath string) (*models.GithubRepository, error) { + gitlabRepo, err := s.gitV2Repository.GitLabGetRepositoryByName(ctx, repoNameWithPath) + if err != nil || gitlabRepo == nil { + return nil, fmt.Errorf("unable to locate GitLab repo for repoNameWithPath : %s, failed : %v", repoNameWithPath, err) + } + + return gitlabRepo.ToGitHubModel(), nil +} + +type UserSigned struct { + signed bool + err error +} + +func (s *service) hasUserSigned(ctx context.Context, claGroupID string, gitlabUser *gitlab.User) (bool, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab-activity.service.hasUserSigned", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabUserID": gitlabUser.ID, + "gitlabUserName": gitlabUser.Username, + "gitlabUserEmail": gitlabUser.Email, + } + + userModels, lookUpErr := s.findUserModelForGitlabUser(f, gitlabUser) + if lookUpErr != nil { + log.WithFields(f).WithError(lookUpErr).Warnf("unable to find user model for gitlab user: %v", gitlabUser) + return false, lookUpErr + } + + if len(userModels) == 0 { + log.WithFields(f).Warnf("gitlab user: %s (%d) not found in easycla records", gitlabUser.Username, gitlabUser.ID) + return false, missingID + } + + for _, userModel := range userModels { + signed, err := s.isSigned(ctx, userModel, claGroupID, gitlabUser) + if err != nil { + log.WithFields(f).Debugf("error checking if user is signed, error: %v", err) + continue + } + if signed { + log.WithFields(f).Debugf("found signed user for clagroupID: %s, userID: %s", claGroupID, userModel.UserID) + return true, nil + } else { + log.WithFields(f).Debugf("user is not signed for claGroupID: %s, userID: %s", claGroupID, userModel.UserID) + } + } + + return false, nil +} + +func (s *service) isSigned(ctx context.Context, userModel *models.User, claGroupID string, gitlabUser *gitlab.User) (bool, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab-activity.service.isSigned", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabUserID": gitlabUser.ID, + "gitlabUserName": gitlabUser.Username, + "gitlabUserEmail": gitlabUser.Email, + } + + // First check for an ICLA signature + icla, err := s.signaturesRepository.GetIndividualSignature(ctx, claGroupID, userModel.UserID, aws.Bool(true), aws.Bool(true)) + if err != nil { + log.WithFields(f).Warnf("fetching ICLA for gitlab user : %d:%s failed : %v", gitlabUser.ID, gitlabUser.Username, err) + return false, err + } + + if icla != nil { + log.WithFields(f).Infof("user has signed the following signature (ICLA): %s, passing", icla.SignatureID) + return true, nil + } + + if userModel.CompanyID == "" { + log.WithFields(f).Debugf("user does not have association with any company, can't confirm employee acknoledgement") + return false, fmt.Errorf("user hasn't signed yet") + } + + companyID := userModel.CompanyID + _, err = s.companyRepository.GetCompany(ctx, companyID) + if err != nil { + msg := fmt.Sprintf("can't load company record: %s for user: %s (%s), error: %v", companyID, userModel.Username, userModel.UserID, err) + log.WithFields(f).Errorf(msg) + return false, fmt.Errorf(msg) + } + + corporateSignature, err := s.signatureRepository.GetCorporateSignature(ctx, claGroupID, companyID, aws.Bool(true), aws.Bool(true)) + if err != nil { + msg := fmt.Sprintf("can't load company signature record for company: %s for user : %s (%s), error : %v", companyID, userModel.Username, userModel.UserID, err) + log.WithFields(f).Errorf(msg) + return false, fmt.Errorf(msg) + } + + if corporateSignature == nil { + msg := fmt.Sprintf("no corporate signature (CCLA) record found for company : %s ", companyID) + log.WithFields(f).Errorf(msg) + return false, fmt.Errorf(msg) + } + + log.WithFields(f).Debugf("loaded corporate signature id: %s for claGroupID: %s and companyID: %s", corporateSignature.SignatureID, claGroupID, companyID) + + approvalCriteria := &signatures.ApprovalCriteria{} + if gitlabUser.Email != "" { + approvalCriteria.UserEmail = gitlabUser.Email + } else if gitlabUser.Username != "" { + approvalCriteria.GitlabUsername = gitlabUser.Username + } else { + msg := fmt.Sprintf("gitlabUser model doesn't have enough information to fetch the employee signatures for user : %s", userModel.UserID) + log.WithFields(f).Errorf(msg) + return false, fmt.Errorf(msg) + } + + if !s.IsUserApprovedForSignature(ctx, f, corporateSignature, userModel, gitlabUser) { + log.WithFields(f).Debugf("user is not approved in signature : %s", corporateSignature.SignatureID) + return false, fmt.Errorf("user is not approved in signature : %s", corporateSignature.SignatureID) + } + + employeeSignatures, err := s.signaturesRepository.GetProjectCompanyEmployeeSignatures(ctx, signatures1.GetProjectCompanyEmployeeSignaturesParams{ + CompanyID: companyID, + ProjectID: claGroupID, + PageSize: utils.Int64(100), + }, approvalCriteria) + + if err != nil { + msg := fmt.Sprintf("can't load employee signature records : %s for user : %s association : %v", companyID, userModel.UserID, err) + log.WithFields(f).Errorf(msg) + return false, fmt.Errorf(msg) + } + + if len(employeeSignatures.Signatures) == 0 { + msg := fmt.Sprintf("no employee signature records found for company : %s user : %s association", companyID, userModel.UserID) + log.WithFields(f).Errorf(msg) + return false, fmt.Errorf(msg) + } + + log.WithFields(f).Warnf("is in signature approval list : %s and has employee signature", corporateSignature.SignatureID) + return true, nil +} + +// findUserModelForGitlabUser locates the user model in our users table for the given GitLab user (by GitLab ID, GitLab username, or email) +func (s *service) findUserModelForGitlabUser(f logrus.Fields, gitlabUser *gitlab.User) ([]*models.User, error) { + + if gitlabUser.ID != 0 { + log.WithFields(f).Debugf("Looking up GitLab user via ID: %d", gitlabUser.ID) + userModel, lookupErr := s.usersRepository.GetUserByGitlabID(gitlabUser.ID) + if lookupErr != nil { + log.WithFields(f).WithError(lookupErr).Warnf("problem locating GitLab user via GitLab ID : %d", gitlabUser.ID) + } else if userModel != nil { + log.WithFields(f).Debugf("located GitLab user via ID: %d", gitlabUser.ID) + return []*models.User{userModel}, nil + } + } + + if gitlabUser.Username != "" { + log.WithFields(f).Debugf("Looking up GitLab user via username: %s", gitlabUser.Username) + userModel, lookupErr := s.usersRepository.GetUserByGitLabUsername(gitlabUser.Username) + if lookupErr != nil { + log.WithFields(f).WithError(lookupErr).Warnf("problem locating GitLab user via GitLab username : %s", gitlabUser.Username) + } else if userModel != nil { + log.WithFields(f).Debugf("located GitLab user via username: %s", gitlabUser.Username) + return []*models.User{userModel}, nil + } + } + + if gitlabUser.Email != "" { + gitlabUsers := make([]*models.User, 0) + log.WithFields(f).Debugf("Looking up GitLab user via user email: %s", gitlabUser.Email) + // previously search was done by lf_email, now we are searching by email #3816 + users, lookupErr := s.usersRepository.GetUsersByEmail(gitlabUser.Email) + if lookupErr != nil { + log.WithFields(f).WithError(lookupErr).Warnf("problem locating GitLab user via GitLab username : %s", gitlabUser.Username) + } else if len(users) > 0 { + log.WithFields(f).Debugf("located GitLab user via email: %s", gitlabUser.Email) + gitlabUsers = append(gitlabUsers, users...) + return gitlabUsers, nil + } + } + + // Didn't find it + return nil, nil +} + +func (s *service) IsUserApprovedForSignature(ctx context.Context, f logrus.Fields, corporateSignature *models.Signature, user *models.User, gitlabUser *gitlab.User) bool { + log.WithFields(f).Debugf("checking if user : %s is approved for corporate signature : %s", user.UserID, corporateSignature.SignatureID) + userEmails := user.Emails + if string(user.LfEmail) != "" { + userEmails = append(userEmails, string(user.LfEmail)) + } + + emailApprovalList := corporateSignature.EmailApprovalList + domainApprovalList := corporateSignature.DomainApprovalList + log.WithFields(f).Debugf("checking if user : %s is approved for corporate signature : %s, email approval list : %+v", user.UserID, corporateSignature.SignatureID, emailApprovalList) + + if len(userEmails) > 0 && len(emailApprovalList) > 0 { + for _, email := range userEmails { + for _, approvalEmail := range emailApprovalList { + if email == approvalEmail { + log.WithFields(f).Debugf("found user email : %s in email approval list ", email) + return true + } + } + } + } else { + log.WithFields(f).Warnf("no match for user in signature email approval list") + } + + if len(domainApprovalList) > 0 && len(userEmails) > 0 { + log.WithFields(f).Debugf("checking if emails : %+v are approved for corporate signature : %s, domain approval list : %+v", userEmails, corporateSignature.SignatureID, domainApprovalList) + for _, userEmail := range userEmails { + for _, domainApprovalPattern := range domainApprovalList { + if strings.HasPrefix(domainApprovalPattern, "*.") { + domainApprovalPattern = strings.Replace(domainApprovalPattern, "*.", ".*", 1) + } else if strings.HasPrefix(domainApprovalPattern, "*") { + domainApprovalPattern = strings.Replace(domainApprovalPattern, "*", ".*", 1) + } else if strings.HasPrefix(domainApprovalPattern, ".") { + domainApprovalPattern = strings.Replace(domainApprovalPattern, ".", ".*", 1) + } + regexpApprovalPattern := "^.*@" + domainApprovalPattern + "$" + if ok, err := regexp.MatchString(regexpApprovalPattern, userEmail); ok && err == nil { + log.WithFields(f).Debugf("found user email : %s in email approval list : %s", userEmail, domainApprovalPattern) + return true + } + } + } + } + + gitlabUserName := gitlabUser.Username + gitlabUsernameApprovalList := corporateSignature.GitlabUsernameApprovalList + if gitlabUserName != "" && len(gitlabUsernameApprovalList) > 0 { + log.WithFields(f).Debugf("checking gitlab username : %s for gitlab approval list : %+v", gitlabUserName, gitlabUsernameApprovalList) + for _, gitlabApproval := range gitlabUsernameApprovalList { + if gitlabApproval == gitlabUserName { + log.WithFields(f).Debugf("found gitlab username : %s in gitlab approval list ", gitlabUserName) + return true + } + } + + } else { + log.WithFields(f).Warnf("no match found for gitlabUser : %s in gitlab approval list : %+v", gitlabUserName, gitlabUsernameApprovalList) + } + + gitlabGroupApprovalList := corporateSignature.GitlabOrgApprovalList + if gitlabUserName != "" && len(gitlabGroupApprovalList) > 0 { + log.WithFields(f).Debugf("checking gitlab username : %s for gitlab org approval list : %+v ", gitlabUserName, gitlabGroupApprovalList) + + for _, gitlabGroupApproval := range gitlabGroupApprovalList { + isApproved, err := s.checkGitLabGroupApproval(ctx, gitlabUserName, gitlabGroupApproval) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to get username") + break + } + if isApproved == true { + log.WithFields(f).Debugf(" found gitlab username : %s in gitlab org approval list : %+v", gitlabUserName, gitlabGroupApprovalList) + return true + } + } + } + + log.WithFields(f).Errorf("unable to find user in any approval list") + return false + +} + +/** + * Parses url with the given regular expression and returns the + * group values defined in the expression. + * + */ +func getParams(regEx, url string) (paramsMap map[string]string) { + + var compRegEx = regexp.MustCompile(regEx) + match := compRegEx.FindStringSubmatch(url) + + paramsMap = make(map[string]string) + for i, name := range compRegEx.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + return paramsMap +} + +func (s *service) checkGitLabGroupApproval(ctx context.Context, userName, URL string) (bool, error) { + f := logrus.Fields{ + "functionName": "checkGitLabGroupApproval", + "userName": userName, + "group_url": URL, + } + + log.WithFields(f).Debugf("checking approval list gitlab org criteria : %s for user: %s ", URL, userName) + var searchURL = URL + params := getParams(`(?P\bhttps://gitlab.com/\b)(?P\bgroups\/\b)?(?P\w+)`, URL) + if params[`group`] == "" { + params[`group`] = "groups/" + updated := fmt.Sprintf("%s%s%s", params[`base`], params[`group`], params[`name`]) + log.WithFields(f).Debugf("updating url : %s to %s for easycla search purporses ", searchURL, updated) + searchURL = updated + } + gitlabOrg, _ := s.gitlabOrgService.GetGitLabOrganizationByURL(ctx, searchURL) + if gitlabOrg != nil { + oauthResponse, err := s.gitlabOrgService.RefreshGitLabOrganizationAuth(ctx, common.ToCommonModel(gitlabOrg)) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem refreshing gitlab auth for org: %s ", gitlabOrg.OrganizationName) + return false, err + } + + gitlabClient, clientErr := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if clientErr != nil { + log.WithFields(f).WithError(clientErr).Warnf("problem getting gitLabClient for org: %s ", gitlabOrg.OrganizationName) + return false, clientErr + } + members, err := gitlab_api.ListGroupMembers(ctx, gitlabClient, int(gitlabOrg.OrganizationExternalID)) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem getting gitlab group members") + return false, err + } + for _, member := range members { + if userName == member.Username { + log.WithFields(f).Debugf("%s is a member of group: %s ", userName, URL) + return true, nil + } + } + } + + return false, nil +} diff --git a/cla-backend-go/v2/gitlab-activity/service_test.go b/cla-backend-go/v2/gitlab-activity/service_test.go new file mode 100644 index 000000000..761840bf1 --- /dev/null +++ b/cla-backend-go/v2/gitlab-activity/service_test.go @@ -0,0 +1,203 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_activity + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/xanzy/go-gitlab" +) + +const enabled = false //nolint + +func TestIsUserApprovedForSignature(t *testing.T) { + if enabled { + userModel := &models.User{ + Emails: []string{ + "one@example.com", + "two@bar.com", + }, + } + gitlabUser := &gitlab.User{ + Username: "one", + } + + testCases := []struct { + name string + signature *models.Signature + expected bool + }{ + { + name: "nothing matched", + signature: &models.Signature{}, + }, + { + name: "email approval list non empty no match", + signature: &models.Signature{ + EmailApprovalList: []string{"three@example.com"}, + }, + }, + { + name: "email approval list match", + signature: &models.Signature{ + EmailApprovalList: []string{"one@example.com"}, + }, + expected: true, + }, + { + name: "domain approval list match no match", + signature: &models.Signature{ + DomainApprovalList: []string{"*.foo.com"}, + }, + expected: false, + }, + { + name: "domain approval list match domain star", + signature: &models.Signature{ + DomainApprovalList: []string{"*.example.com"}, + }, + expected: true, + }, + { + name: "domain approval list match domain star globbing", + signature: &models.Signature{ + DomainApprovalList: []string{"*example.com"}, + }, + expected: true, + }, + { + name: "domain approval list match domain star dot", + signature: &models.Signature{ + DomainApprovalList: []string{".example.com"}, + }, + expected: true, + }, + { + name: "gitlab username approval list no match", + signature: &models.Signature{ + GitlabUsernameApprovalList: []string{"two"}, + }, + expected: false, + }, + { + name: "gitlab username approval list match", + signature: &models.Signature{ + GitlabUsernameApprovalList: []string{"one"}, + }, + expected: true, + }, + } + activityService := NewService(nil, nil, nil, nil, nil, nil, nil, nil) + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + result := activityService.IsUserApprovedForSignature(context.Background(), logrus.Fields{}, tc.signature, userModel, gitlabUser) + if tc.expected { + assert.True(tt, result) + + } else { + assert.False(tt, result) + } + }) + } + } + +} + +func TestPrepareMrCommentContent(t *testing.T) { + if enabled { + signedContains := ":white_check_mark: %s" + missingUserContains := ":x: The commit associated with %s is missing the User's ID" + missingAffiliationContains := "%s is authorized, but they must confirm their affiliation" + missingApprovalContains := "%s's commit is not authorized under a signed CLA" + + testCases := []struct { + name string + signed []*gitlab.User + missing []*gatedGitlabUser + expectedMsgs []string + expectedBadge string + }{ + { + name: "all signed", + signed: []*gitlab.User{ + {ID: 1, Username: "neo"}, + {ID: 2, Username: "oracle"}, + }, + expectedMsgs: []string{signedContains, signedContains}, + expectedBadge: "cla-signed.svg", + }, + { + name: "missing id", + signed: []*gitlab.User{ + {ID: 1, Username: "neo"}, + }, + missing: []*gatedGitlabUser{ + {err: missingID, User: &gitlab.User{ID: 3, Username: "missing"}}, + }, + expectedMsgs: []string{signedContains, missingUserContains}, + expectedBadge: "cla-missing-id.svg", + }, + { + name: "missing affiliation", + signed: []*gitlab.User{ + {ID: 1, Username: "neo"}, + }, + missing: []*gatedGitlabUser{ + {err: missingCompanyAffiliation, User: &gitlab.User{ID: 4, Username: "affiliationUser"}}, + }, + expectedMsgs: []string{signedContains, missingAffiliationContains}, + expectedBadge: "cla-confirmation-needed.svg", + }, + { + name: "missing approval", + signed: []*gitlab.User{ + {ID: 1, Username: "neo"}, + }, + missing: []*gatedGitlabUser{ + {err: missingCompanyApproval, User: &gitlab.User{ID: 5, Username: "approvalUser"}}, + }, + expectedMsgs: []string{signedContains, missingApprovalContains}, + expectedBadge: "cla-not-signed.svg", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(tt *testing.T) { + result := PrepareMrCommentContent(tc.missing, tc.signed, "https://sign.com") + tt.Logf("the result is : %s", result) + parts := strings.Split(result, "
  • ") + assert.Len(tt, parts, len(tc.expectedMsgs)+1) + + var allUsers []*gitlab.User + + if len(tc.signed) > 0 { + for _, s := range tc.signed { + allUsers = append(allUsers, s) + } + } + + if len(tc.missing) > 0 { + for _, m := range tc.missing { + allUsers = append(allUsers, m.User) + } + } + + for i, p := range parts[1:] { + expected := fmt.Sprintf(tc.expectedMsgs[i], getAuthorInfo(allUsers[i])) + assert.Contains(tt, p, expected) + } + + assert.Contains(tt, result, tc.expectedBadge) + }) + } + } + +} diff --git a/cla-backend-go/v2/gitlab_organizations/constants.go b/cla-backend-go/v2/gitlab_organizations/constants.go new file mode 100644 index 000000000..ca2ba4226 --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/constants.go @@ -0,0 +1,41 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +const ( + // GitLabOrganizationsProjectSFIDColumn constant + GitLabOrganizationsProjectSFIDColumn = "project_sfid" + // GitLabOrganizationsOrganizationIDColumn constant + GitLabOrganizationsOrganizationIDColumn = "organization_id" + // GitLabOrganizationsOrganizationSFIDColumn constant + GitLabOrganizationsOrganizationSFIDColumn = "organization_sfid" + // GitLabOrganizationsOrganizationNameColumn constant + GitLabOrganizationsOrganizationNameColumn = "organization_name" + // GitLabOrganizationsOrganizationNameLowerColumn constant + GitLabOrganizationsOrganizationNameLowerColumn = "organization_name_lower" + // GitLabOrganizationsEnabledColumn constant + GitLabOrganizationsEnabledColumn = "enabled" + // GitLabOrganizationsAutoEnabledColumn constant + GitLabOrganizationsAutoEnabledColumn = "auto_enabled" + // GitLabOrganizationsAutoEnabledCLAGroupIDColumn constant + GitLabOrganizationsAutoEnabledCLAGroupIDColumn = "auto_enabled_cla_group_id" + // GitLabOrganizationsBranchProtectionEnabledColumn constant + GitLabOrganizationsBranchProtectionEnabledColumn = "branch_protection_enabled" + // GitLabOrganizationsAuthInfoColumn constant + GitLabOrganizationsAuthInfoColumn = "auth_info" + // GitLabOrganizationsOrganizationURLColumn constant + GitLabOrganizationsOrganizationURLColumn = "organization_url" + // GitLabOrganizationsOrganizationFullPathColumn constant + GitLabOrganizationsOrganizationFullPathColumn = "organization_full_path" + // GitLabOrganizationsNoteColumn constant + GitLabOrganizationsNoteColumn = "note" + // GitLabOrganizationsDateCreatedColumn constant + GitLabOrganizationsDateCreatedColumn = "date_created" + // GitLabOrganizationsDateModifiedColumn constant + GitLabOrganizationsDateModifiedColumn = "date_modified" + // GitLabOrganizationsExternalGitLabGroupIDColumn constant + GitLabOrganizationsExternalGitLabGroupIDColumn = "external_gitlab_group_id" + // GitLabOrganizationsAuthExpiryTimeColumn constant + GitLabOrganizationsAuthExpiryTimeColumn = "auth_expiry_time" +) diff --git a/cla-backend-go/v2/gitlab_organizations/handlers.go b/cla-backend-go/v2/gitlab_organizations/handlers.go new file mode 100644 index 000000000..33f3a75f9 --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/handlers.go @@ -0,0 +1,683 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + "github.com/go-openapi/runtime" + + projectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/gitlab_activity" + gitlabApi "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/gofrs/uuid" + + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" + + "github.com/LF-Engineering/lfx-kit/auth" + "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_organizations" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/go-openapi/runtime/middleware" + "github.com/savaki/dynastore" +) + +const ( + // SessionStoreKey for cla-gitlab + SessionStoreKey = "cla-gitlab" +) + +// Configure setups handlers on api with service +func Configure(api *operations.EasyclaAPI, service ServiceInterface, eventService events.Service, sessionStore *dynastore.Store, contributorConsoleV2Base string) { + + api.GitlabOrganizationsGetProjectGitlabOrganizationsHandler = gitlab_organizations.GetProjectGitlabOrganizationsHandlerFunc( + func(params gitlab_organizations.GetProjectGitlabOrganizationsParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.handlers.GitlabOrganizationsGetProjectGitlabOrganizationsHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + } + + // Load the project + psc := projectService.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return gitlab_organizations.NewGetProjectGitlabOrganizationsNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Get Project GitLab Organizations for Project '%s' with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewGetProjectGitlabOrganizationsForbidden().WithPayload( + utils.ErrorResponseForbidden(reqID, msg)) + } + + result, err := service.GetGitLabOrganizationsByProjectSFID(ctx, params.ProjectSFID) + if err != nil { + if strings.ContainsAny(err.Error(), "getProjectNotFound") { + msg := fmt.Sprintf("Gitlab organization with project SFID not found: %s", params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewGetProjectGitlabOrganizationsNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, msg)) + } + + msg := fmt.Sprintf("failed to locate Gitlab organization by project SFID: %s, error: %+v", params.ProjectSFID, err) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewGetProjectGitlabOrganizationsBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + return gitlab_organizations.NewGetProjectGitlabOrganizationsOK().WithPayload(result) + }) + + api.GitlabOrganizationsAddProjectGitlabOrganizationHandler = gitlab_organizations.AddProjectGitlabOrganizationHandlerFunc( + func(params gitlab_organizations.AddProjectGitlabOrganizationParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.handlers.GitlabOrganizationsAddProjectGitlabOrganizationHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + "groupID": params.Body.GroupID, + "groupFullPath": params.Body.OrganizationFullPath, + } + + // Load the project + psc := projectService.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return gitlab_organizations.NewAddProjectGitlabOrganizationForbidden().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + // Load the project parent + parentProjectModel, err := psc.GetParentProjectModel(params.ProjectSFID) + if err != nil || (parentProjectModel == nil && !utils.IsProjectHasRootParent(projectModel)) { + return gitlab_organizations.NewAddProjectGitlabOrganizationForbidden().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate parent project from project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Add Project GitLab Organizations for Project '%s' with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationForbidden().WithPayload( + utils.ErrorResponseForbidden(reqID, msg)) + } + + // Quick check of the parameters + if params.Body == nil || (params.Body.GroupID == 0 && params.Body.OrganizationFullPath == "") { + msg := fmt.Sprintf("missing group ID or group full path in the body: %+v", params.Body) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + orgURL := params.Body.OrganizationFullPath + // Clean up/filter the Group Full Path, if needed + if params.Body.OrganizationFullPath != "" { + r, regexErr := regexp.Compile(`^http(s)?://`) + if regexErr != nil { + msg := fmt.Sprintf("invalid for group/organization full path, error: %+v", regexErr) + log.WithFields(f).WithError(regexErr).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationInternalServerError().WithPayload( + utils.ErrorResponseInternalServerErrorWithError(reqID, msg, regexErr)) + } + if r.MatchString(params.Body.OrganizationFullPath) { + groupWithUrl, urlParseErr := url.Parse(params.Body.OrganizationFullPath) + if urlParseErr != nil { + msg := fmt.Sprintf("invalid group full path provided, error: %+v", urlParseErr) + log.WithFields(f).WithError(urlParseErr).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, urlParseErr)) + } + // Update the group full path value - just include the path and not the https://... part + params.Body.OrganizationFullPath = cleanPath(groupWithUrl.Path) + log.WithFields(f).Debug(fmt.Sprintf("updated full path: %s ", params.Body.OrganizationFullPath)) + } + + // Remove leading slash + if strings.HasPrefix(params.Body.OrganizationFullPath, "/") { + params.Body.OrganizationFullPath = params.Body.OrganizationFullPath[1:] + } + + if strings.HasSuffix(params.Body.OrganizationFullPath, "/") { + log.WithFields(f).Debugf("Trimming suffix for : %s", params.Body.OrganizationFullPath) + params.Body.OrganizationFullPath = strings.TrimSuffix(params.Body.OrganizationFullPath, "/") + } + } + + if params.Body.AutoEnabled == nil { + msg := fmt.Sprintf("missing autoEnabled name in body: %+v", params.Body) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + f["autoEnabled"] = utils.BoolValue(params.Body.AutoEnabled) + f["autoEnabledClaGroupID"] = params.Body.AutoEnabledClaGroupID + + if !utils.ValidateAutoEnabledClaGroupID(*params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + msg := "AutoEnabledClaGroupID can't be empty when AutoEnabled" + err := fmt.Errorf(msg) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + // If the parent is TLF, then use the same project SFID value for the parent SFID value + parentProjectSFID := "" + if parentProjectModel != nil { + parentProjectSFID = parentProjectModel.ID + } + if utils.IsProjectHasRootParent(projectModel) { + parentProjectSFID = params.ProjectSFID + } + + // Convert the various input parameters and values to an add GitLab Group/Org model + inputModel := &common.GitLabAddOrganization{ + ProjectSFID: params.ProjectSFID, + ParentProjectSFID: parentProjectSFID, // could be the same SFID as the project SFID if parent is TLF + AutoEnabled: utils.BoolValue(params.Body.AutoEnabled), + AutoEnabledClaGroupID: params.Body.AutoEnabledClaGroupID, + BranchProtectionEnabled: utils.BoolValue(params.Body.BranchProtectionEnabled), + ExternalGroupID: params.Body.GroupID, + OrganizationURL: orgURL, + OrganizationFullPath: params.Body.OrganizationFullPath, + } + + result, err := service.AddGitLabOrganization(ctx, inputModel) + if err != nil { + if _, ok := err.(*utils.ProjectConflict); ok { + return gitlab_organizations.NewAddProjectGitlabOrganizationConflict().WithPayload( + utils.ErrorResponseConflict(reqID, err.Error())) + } + msg := fmt.Sprintf("unable to add GitLab organization, error: %+v", err) + log.WithFields(f).WithError(err).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + // Get the current group name for the event + for _, group := range result.List { + if group.OrganizationExternalID == params.Body.GroupID || group.OrganizationFullPath == params.Body.OrganizationFullPath { + // Log the event + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + LfUsername: authUser.UserName, + EventType: events.GitlabOrganizationAdded, + ProjectSFID: params.ProjectSFID, + EventData: &events.GitLabOrganizationAddedEventData{ + GitLabOrganizationName: group.OrganizationName, + }, + }) + } + } + + return gitlab_organizations.NewAddProjectGitlabOrganizationOK().WithPayload(result) + }) + + api.GitlabOrganizationsGetGitLabGroupMembersHandler = gitlab_organizations.GetGitLabGroupMembersHandlerFunc(func(params gitlab_organizations.GetGitLabGroupMembersParams) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.handlers.GitlabOrganizationsGetGitLabGroupMembersHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabGroupID": params.GitLabGroupID, + } + log.WithFields(f).Debug("fetching gitlab group member details") + memberList, err := service.GetGitLabGroupMembers(ctx, params.GitLabGroupID) + if err != nil { + msg := fmt.Sprintf("unable to get groupID: %s member list: %+v ", params.GitLabGroupID, err) + return gitlab_organizations.NewGetGitLabGroupMembersBadRequest().WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + } + return gitlab_organizations.NewGetGitLabGroupMembersOK().WithPayload(memberList) + }) + + api.GitlabOrganizationsUpdateProjectGitlabGroupConfigHandler = gitlab_organizations.UpdateProjectGitlabGroupConfigHandlerFunc(func(params gitlab_organizations.UpdateProjectGitlabGroupConfigParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.handlers.GitlabOrganizationsAddProjectGitlabOrganizationHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + "gitLabGroupID": params.GitLabGroupID, + "autoEnabled": params.Body.AutoEnabled, + "autoEnabledCLAGroupID": params.Body.AutoEnabledClaGroupID, + "branchProtectionEnabled": params.Body.BranchProtectionEnabled, + } + + // Load the project + psc := projectService.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + // Load the project parent + parentProjectModel, err := psc.GetParentProjectModel(params.ProjectSFID) + if err != nil || parentProjectModel == nil { + msg := fmt.Sprintf("unable to locate parent project from project with ID: %s", params.ProjectSFID) + log.WithFields(f).Warn(msg) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Update Project GitLab Group/Organizations for Project '%s' with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigForbidden().WithPayload( + utils.ErrorResponseForbidden(reqID, msg)) + } + + if !utils.ValidateAutoEnabledClaGroupID(params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + msg := "AutoEnabledClaGroupID can't be empty when AutoEnabled is set to true" + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigBadRequest().WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + } + + inputModel := &common.GitLabAddOrganization{ + ProjectSFID: params.ProjectSFID, + AutoEnabled: params.Body.AutoEnabled, + AutoEnabledClaGroupID: params.Body.AutoEnabledClaGroupID, + BranchProtectionEnabled: params.Body.BranchProtectionEnabled, + ExternalGroupID: params.GitLabGroupID, + Enabled: true, + } + + if parentProjectModel != nil { + inputModel.ParentProjectSFID = parentProjectModel.ID + } + + updateErr := service.UpdateGitLabOrganization(ctx, inputModel) + if updateErr != nil { + if errors.Is(updateErr, projects_cla_groups.ErrCLAGroupDoesNotExist) { + msg := fmt.Sprintf("problem updating GitLab group/organization for project %s with SFID: %s - CLA Group wth ID: %s was not found, error: %+v", projectModel.Name, projectModel.ID, params.Body.AutoEnabledClaGroupID, updateErr) + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigNotFound().WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } + msg := fmt.Sprintf("problem updating GitLab group/organization for project %s with SFID: %s, error: %+v", projectModel.Name, projectModel.ID, updateErr) + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigBadRequest().WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, updateErr)) + } + + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.GitlabOrganizationUpdated, + ProjectSFID: params.ProjectSFID, + ProjectName: projectModel.Name, + CLAGroupID: params.Body.AutoEnabledClaGroupID, + LfUsername: authUser.UserName, + UserName: authUser.UserName, + EventData: &events.GitLabOrganizationUpdatedEventData{ + GitLabGroupID: params.GitLabGroupID, + AutoEnabledClaGroupID: params.Body.AutoEnabledClaGroupID, + AutoEnabled: params.Body.AutoEnabled, + }, + }) + + results, err := service.GetGitLabOrganizationsByProjectSFID(ctx, params.ProjectSFID) + if err != nil { + if strings.ContainsAny(err.Error(), "getProjectNotFound") { + msg := fmt.Sprintf("Gitlab organization with project SFID not found: %s", params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigNotFound().WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } + + msg := fmt.Sprintf("failed to locate Gitlab organization by project SFID: %s, error: %+v", params.ProjectSFID, err) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigBadRequest().WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + return gitlab_organizations.NewUpdateProjectGitlabGroupConfigOK().WithPayload(results) + }) + + api.GitlabOrganizationsDeleteProjectGitlabGroupConfigHandler = gitlab_organizations.DeleteProjectGitlabGroupConfigHandlerFunc(func(params gitlab_organizations.DeleteProjectGitlabGroupConfigParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.handlers.GitlabOrganizationsDeleteProjectGitlabGroupConfigHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSFID, + "organizationFullPath": params.OrganizationFullPath, + "authUser": authUser.UserName, + "authEmail": authUser.Email, + } + + // Load the project + psc := projectService.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return gitlab_organizations.NewDeleteProjectGitlabGroupConfigNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Delete Project GitLab Group/Organizations for Project '%s' with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewDeleteProjectGitlabGroupConfigForbidden().WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + + err = service.DeleteGitLabOrganizationByFullPath(ctx, params.ProjectSFID, params.OrganizationFullPath) + if err != nil { + if strings.Contains(err.Error(), "getProjectNotFound") { + msg := fmt.Sprintf("project not found with given SFID: %s", params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewDeleteProjectGitlabGroupConfigNotFound().WithPayload(utils.ErrorResponseNotFoundWithError(reqID, msg, err)) + } + msg := fmt.Sprintf("problem deleting Gitlab Group with project SFID: %s with path: %s", params.ProjectSFID, params.OrganizationFullPath) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewDeleteProjectGitlabGroupConfigBadRequest().WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + LfUsername: authUser.UserName, + EventType: events.GitlabOrganizationDeleted, + ProjectSFID: params.ProjectSFID, + EventData: &events.GitLabOrganizationDeletedEventData{ + GitLabOrganizationName: params.OrganizationFullPath, + }, + }) + + return gitlab_organizations.NewDeleteProjectGitlabGroupConfigNoContent() + }) + + api.GitlabActivityGitlabOauthCallbackHandler = gitlab_activity.GitlabOauthCallbackHandlerFunc(func(params gitlab_activity.GitlabOauthCallbackParams) middleware.Responder { + ctx := utils.NewContext() + f := logrus.Fields{ + "functionName": "gitlab_organization.handlers.GitlabActivityGitlabOauthCallbackHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "code": params.Code, + "state": params.State, + } + + requestID, _ := uuid.NewV4() + reqID := requestID.String() + + if params.Code == nil || params.State == nil { + msg := "missing code or state parameter" + log.WithFields(f).Warn(msg) + return middleware.ResponderFunc( + func(rw http.ResponseWriter, p runtime.Producer) { + session, err := sessionStore.Get(params.HTTPRequest, SessionStoreKey) + if err != nil { + log.WithFields(f).WithError(err).Warn("error with session store lookup") + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + gitlabOriginURL, ok := session.Values["gitlab_origin_url"].(string) + if !ok { + msg := "Error getting gitlab_origin_url - missing from session object" + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + http.Redirect(rw, params.HTTPRequest, gitlabOriginURL, http.StatusSeeOther) + }) + } + + codeParts := strings.Split(*params.State, ":") + if len(codeParts) != 2 { + msg := fmt.Sprintf("invalid state variable passed : %s", *params.State) + log.WithFields(f).Warn(msg) + return NewServerError(reqID, "", errors.New(msg)) + } + + if codeParts[0] == "user" { + // handle authorization + return middleware.ResponderFunc( + func(rw http.ResponseWriter, p runtime.Producer) { + session, err := sessionStore.Get(params.HTTPRequest, SessionStoreKey) + if err != nil { + log.WithFields(f).WithError(err).Warn("error with session store lookup") + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + log.WithFields(f).Debugf("Loaded session: %+v", session.Values) + state, ok := session.Values["gitlab_oauth2_state"].(string) + if !ok { + msg := "Error getting session state - missing from session object" + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + gitlabOriginURL, ok := session.Values["gitlab_origin_url"].(string) + if !ok { + msg := "Error getting gitlab_origin_url - missing from session object" + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + repositoryID, ok := session.Values["gitlab_repository_id"].(string) + if !ok { + msg := "Error getting gitlab_repository_id - missing from session object" + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + mergeRequestID, ok := session.Values["gitlab_merge_request_id"].(string) + if !ok { + msg := "Error getting gitlab_merge_request_id - missing from session object" + log.WithFields(f).Warn(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + if *params.State != state { + msg := fmt.Sprintf("mismatch state, received: %s from callback, but loaded our state as: %s", + *params.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 := gitlabApi.FetchOauthCredentials(*params.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(params.HTTPRequest, rw) + + // Get client + gitlabClient, err := gitlabApi.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 := service.InitiateSignRequest(ctx, params.HTTPRequest, gitlabClient, repositoryID, mergeRequestID, gitlabOriginURL, contributorConsoleV2Base, eventService) + log.WithFields(f).Debugf("redirecting to :%s ", *consoleURL) + http.Redirect(rw, params.HTTPRequest, *consoleURL, http.StatusSeeOther) + }) + } + + gitlabOrganizationID := codeParts[0] + stateVar := codeParts[1] + + gitLabOrg, err := service.GetGitLabOrganizationByState(ctx, gitlabOrganizationID, stateVar) + if err != nil { + msg := fmt.Sprintf("fetching gitlab model failed : %s : %v", gitlabOrganizationID, err) + log.WithFields(f).WithError(err).Warn(msg) + return NewServerError(reqID, "", errors.New(msg)) + } + + // now fetch the oauth credentials and store to db + oauthResp, err := gitlabApi.FetchOauthCredentials(*params.Code) + if err != nil { + msg := fmt.Sprintf("fetching gitlab credentials failed : %s : %v", gitlabOrganizationID, err) + log.WithFields(f).WithError(err).Warn(msg) + return NewServerError(reqID, "", errors.New(msg)) + } + log.WithFields(f).Debugf("oauth resp is like : %+v", oauthResp) + + // track the expiry time of the token + expiryTime := time.Now().Add(time.Duration(oauthResp.ExpiresIn) * time.Second).Unix() + + updateErr := service.UpdateGitLabOrganizationAuth(ctx, gitlabOrganizationID, oauthResp, expiryTime) + if updateErr != nil { + msg := fmt.Sprintf("installation of GitLab Group and Repositories, error: %v", updateErr) + log.WithFields(f).WithError(updateErr).Warn(msg) + return NewServerError(reqID, "", errors.New(fmt.Sprintf("%v", updateErr))) + } + + // Reload the GitLab organization - will have additional details now... + updatedGitLabOrgDBModel, err := service.GetGitLabOrganizationByID(ctx, gitLabOrg.OrganizationID) + if err != nil { + msg := fmt.Sprintf("problem loading updated gitlab organization by ID: %s : %v", gitlabOrganizationID, err) + log.WithFields(f).Errorf(msg) + return NewServerError(reqID, "", errors.New(msg)) + } + + return NewSuccessResponse(reqID, updatedGitLabOrgDBModel.ProjectSFID, updatedGitLabOrgDBModel.OrganizationName) + }) +} + +// SuccessResponse Success +type SuccessResponse struct { + ReqID string + ProjectSFID string + GitLabGroupName string +} + +// NewSuccessResponse creates a new redirect handler +func NewSuccessResponse(reqID, projectSFID, gitLabGroupName string) *SuccessResponse { + return &SuccessResponse{reqID, projectSFID, gitLabGroupName} +} + +// WriteResponse to the client +func (o *SuccessResponse) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + configPage := "https://gitlab.com/-/profile/applications" + + html := fmt.Sprintf(` + + + LFX EasyCLA Service GitLab App Installation Status + + + + + + + + + +
    + lf logo +
    +

    LFx EasyCLA Service GitLab App - Installation Successful

    +

    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(` + + + LFX EasyCLA Service GitLab App Installation Status + + + + + + + + + +
    + lf logo +
    +

    LFx EasyCLA Service GitLab App - Installation Issue

    +

    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 := `(?P\bgroups\/\b)?(?P\w+)` + paramsMap := utils.ParseString(reg, fullPath) + if paramsMap["groups"] != "" { + log.WithFields(f).Debugf("stripping %s from %s", paramsMap["groups"], fullPath) + result = strings.ReplaceAll(fullPath, paramsMap["groups"], "") + } + log.WithFields(f).Debugf("clean path is %s ", result) + return result +} diff --git a/cla-backend-go/v2/gitlab_organizations/repository.go b/cla-backend-go/v2/gitlab_organizations/repository.go new file mode 100644 index 000000000..f059537b8 --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/repository.go @@ -0,0 +1,897 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + "github.com/gofrs/uuid" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "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" + 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/utils" + "github.com/sirupsen/logrus" +) + +// indexes +const ( + // GitLabOrgOrganizationSFIDIndex the index for the Project Parent SFID + GitLabOrgOrganizationSFIDIndex = "gitlab-org-sfid-index" + // GitLabOrgProjectSFIDIndex the index for the Project SFID + GitLabOrgProjectSFIDIndex = "gitlab-project-sfid-index" + // GitLabOrgLowerNameIndex the index for the group/org name in lower case + GitLabOrgLowerNameIndex = "gitlab-organization-name-lower-search-index" + // GitLabExternalIDIndex the index for the external ID + GitLabExternalIDIndex = "gitlab-external-group-id-index" + // GitLabFullPathIndex the index for the full path + GitLabFullPathIndex = "gitlab-full-path-index" + // GitlabOrgURLIndex the index for the org url + GitlabOrgURLIndex = "gitlab-org-url-index" +) + +// RepositoryInterface is interface for gitlab org data model +type RepositoryInterface interface { + AddGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization, enabled bool) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizations(ctx context.Context) (*v2Models.GitlabOrganizations, error) + GetGitLabOrganizationsEnabled(ctx context.Context) (*v2Models.GitlabOrganizations, error) + GetGitLabOrganizationsEnabledWithAutoEnabled(ctx context.Context) (*v2Models.GitlabOrganizations, error) + GetGitLabOrganizationsByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabOrganizations, error) + GetGitLabOrganizationsByFoundationSFID(ctx context.Context, foundationSFID string) (*v2Models.GitlabOrganizations, error) + GetGitLabOrganization(ctx context.Context, gitlabOrganizationID string) (*common.GitLabOrganization, error) + GetGitLabOrganizationByName(ctx context.Context, gitLabOrganizationName string) (*common.GitLabOrganization, error) + GetGitLabOrganizationByExternalID(ctx context.Context, gitLabGroupID int64) (*common.GitLabOrganization, error) + GetGitLabOrganizationByFullPath(ctx context.Context, groupFullPath string) (*common.GitLabOrganization, error) + GetGitLabOrganizationByURL(ctx context.Context, url string) (*common.GitLabOrganization, error) + UpdateGitLabOrganizationAuth(ctx context.Context, organizationID string, gitLabGroupID int, authExpiryTime int64, authInfo, groupName, groupFullPath, organizationURL string) error + UpdateGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization, enabled bool) error + DeleteGitLabOrganizationByFullPath(ctx context.Context, projectSFID, gitlabOrgFullPath string) error +} + +// Repository object/struct +type Repository struct { + stage string + dynamoDBClient *dynamodb.DynamoDB + gitlabOrgTableName string +} + +// NewRepository creates a new instance of the gitlabOrganizations repository +func NewRepository(awsSession *session.Session, stage string) RepositoryInterface { + return &Repository{ + stage: stage, + dynamoDBClient: dynamodb.New(awsSession), + gitlabOrgTableName: fmt.Sprintf("cla-%s-gitlab-orgs", stage), + } +} + +// AddGitLabOrganization adds the specified values to the GitLab Group/Org table +func (repo *Repository) AddGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization, enabled bool) (*v2Models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.AddGitLabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "parentProjectSFID": input.ParentProjectSFID, + "projectSFID": input.ProjectSFID, + "groupID": input.ExternalGroupID, + "organizationName": input.OrganizationName, + "groupFullPath": input.OrganizationFullPath, + "autoEnabled": input.AutoEnabled, + "autoEnabledClaGroupID": input.AutoEnabledClaGroupID, + "branchProtectionEnabled": input.BranchProtectionEnabled, + "enabled": enabled, + } + + var existingRecord *common.GitLabOrganization + var getErr error + if input.ExternalGroupID != 0 { + log.WithFields(f).Debugf("checking to see if we have an existing GitLab organization with ID: %d", input.ExternalGroupID) + // First, let's check to see if we have an existing gitlab organization with the same name + existingRecord, getErr = repo.GetGitLabOrganizationByExternalID(ctx, input.ExternalGroupID) + if getErr != nil { + log.WithFields(f).WithError(getErr).Debugf("unable to locate existing GitLab group by ID: %d - ok to create a new record", input.ExternalGroupID) + } + } else if input.OrganizationFullPath != "" { + log.WithFields(f).Debugf("checking to see if we have an existing GitLab group full path with value: %s", input.OrganizationFullPath) + // First, let's check to see if we have an existing gitlab organization with the same name + existingRecord, getErr = repo.GetGitLabOrganizationByFullPath(ctx, input.OrganizationFullPath) + if getErr != nil { + log.WithFields(f).WithError(getErr).Debugf("unable to locate existing GitLab group by full path: %s - ok to create a new record", input.OrganizationFullPath) + } + } + + if existingRecord != nil { + log.WithFields(f).Debugf("An existing GitLab organization with ID %d or full path: %s exists in our database", input.ExternalGroupID, input.OrganizationFullPath) + // If everything matches... + if input.ProjectSFID == existingRecord.ProjectSFID { + log.WithFields(f).Debug("existing GitLab organization with same SFID - should be able to update it") + updateErr := repo.UpdateGitLabOrganization(ctx, input, enabled) + if updateErr != nil { + return nil, updateErr + } + + if input.ExternalGroupID > 0 { + // Return the updated record + if gitlabOrg, err := repo.GetGitLabOrganizationByExternalID(ctx, input.ExternalGroupID); err != nil { + return nil, err + } else { + return common.ToModel(gitlabOrg), nil + } + } else if input.OrganizationFullPath != "" { + // Return the updated record + if gitlabOrg, err := repo.GetGitLabOrganizationByFullPath(ctx, input.OrganizationFullPath); err != nil { + return nil, err + } else { + return common.ToModel(gitlabOrg), nil + } + } + } + + msg := fmt.Sprintf("record already exists - existing GitLab group with a different project SFID - won't be able to update it") + log.WithFields(f).Debug(msg) + return nil, errors.New(msg) + } + + // No existing records - create one + _, currentTime := utils.CurrentTime() + organizationID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warnf("Unable to generate a UUID for gitlab org, error: %v2Models", err) + return nil, err + } + + authStateNonce, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warnf("Unable to generate a auth nonce UUID for gitlab org, error: %v2Models", err) + return nil, err + } + + gitlabOrg := &common.GitLabOrganization{ + OrganizationID: organizationID.String(), + DateCreated: currentTime, + DateModified: currentTime, + OrganizationName: input.OrganizationName, + OrganizationNameLower: strings.ToLower(input.OrganizationName), + OrganizationURL: input.OrganizationURL, + OrganizationFullPath: input.OrganizationFullPath, + ExternalGroupID: input.ExternalGroupIDAsInt(), + OrganizationSFID: input.ParentProjectSFID, + ProjectSFID: input.ProjectSFID, + Enabled: enabled, + AutoEnabled: input.AutoEnabled, + AutoEnabledClaGroupID: input.AutoEnabledClaGroupID, + BranchProtectionEnabled: input.BranchProtectionEnabled, + AuthState: authStateNonce.String(), + Version: "v1", + } + + log.WithFields(f).Debug("encoding GitLab organization record for adding to the database...") + av, err := dynamodbattribute.MarshalMap(gitlabOrg) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to marshall request for query") + return nil, err + } + + log.WithFields(f).Debug("adding gitlab organization record to the database...") + _, err = repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(repo.gitlabOrgTableName), + ConditionExpression: aws.String("attribute_not_exists(organization_name)"), + }) + if err != nil { + if aErr, ok := err.(awserr.Error); ok { + switch aErr.Code() { + case dynamodb.ErrCodeConditionalCheckFailedException: + log.WithFields(f).WithError(err).Warn("gitlab group/organization already exists") + return nil, fmt.Errorf("gitlab group/organization already exists") + } + } + log.WithFields(f).WithError(err).Warn("cannot put gitlab group/organization in dynamodb") + return nil, err + } + + return common.ToModel(gitlabOrg), nil +} + +// GetGitLabOrganizations returns the complete list of GitLab groups/organizations +func (repo *Repository) GetGitLabOrganizations(ctx context.Context) (*v2Models.GitlabOrganizations, error) { + // No filter, return all + return repo.getScanResults(ctx, nil) +} + +// GetGitLabOrganizationsEnabled returns the list of GitLab groups/organizations that are enabled +func (repo *Repository) GetGitLabOrganizationsEnabled(ctx context.Context) (*v2Models.GitlabOrganizations, error) { + // Build the scan/query expression + filter := expression.Name(GitLabOrganizationsEnabledColumn).Equal(expression.Value(true)) + return repo.getScanResults(ctx, &filter) +} + +// GetGitLabOrganizationsEnabledWithAutoEnabled returns the list of GitLab groups/organizations that are enabled with the auto enabled flag set to true +func (repo *Repository) GetGitLabOrganizationsEnabledWithAutoEnabled(ctx context.Context) (*v2Models.GitlabOrganizations, error) { + // Build the scan/query expression + // Every GitLab organization should now have auto-enabled set to true - so we can just return the enabled list + //filter := expression.Name(GitLabOrganizationsEnabledColumn).Equal(expression.Value(true)). + // And(expression.Name(GitLabOrganizationsAutoEnabledColumn).Equal(expression.Value(true))) + filter := expression.Name(GitLabOrganizationsEnabledColumn).Equal(expression.Value(true)) + return repo.getScanResults(ctx, &filter) +} + +// GetGitLabOrganizationsByFoundationSFID returns the list of GitLab groups/organizations under the given foundation +func (repo *Repository) GetGitLabOrganizationsByFoundationSFID(ctx context.Context, foundationSFID string) (*v2Models.GitlabOrganizations, error) { + // Build the scan/query expression + condition := expression.Key(GitLabOrganizationsOrganizationSFIDColumn).Equal(expression.Value(foundationSFID)) + filter := expression.Name(GitLabOrganizationsEnabledColumn).Equal(expression.Value(true)) + return repo.getOrganizationsWithConditionFilter(ctx, condition, filter, GitLabOrgOrganizationSFIDIndex) +} + +// GetGitLabOrganizationsByProjectSFID get GitLab organizations based on the project SFID or parent project SFID +func (repo *Repository) GetGitLabOrganizationsByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.GetGitLabOrganizationsByProjectSFID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + } + + condition := expression.Key(GitLabOrganizationsProjectSFIDColumn).Equal(expression.Value(projectSFID)) + filter := expression.Name(GitLabOrganizationsEnabledColumn).Equal(expression.Value(true)) + response, err := repo.getOrganizationsWithConditionFilter(ctx, condition, filter, GitLabOrgProjectSFIDIndex) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("Error getting GitLab organizations by project SFID, error: %v2Models", err) + return nil, err + } + + return response, nil +} + +// GetGitLabOrganizationByName get GitLab organization by name +func (repo *Repository) GetGitLabOrganizationByName(ctx context.Context, gitLabOrganizationName string) (*common.GitLabOrganization, error) { + f := logrus.Fields{ + "functionName": "v1.gitlab_organizations.repository.GetGitLabOrganizationByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationName": gitLabOrganizationName, + } + + gitLabOrganizationName = strings.ToLower(gitLabOrganizationName) + + condition := expression.Key(GitLabOrganizationsOrganizationNameLowerColumn).Equal(expression.Value(strings.ToLower(gitLabOrganizationName))) + builder := expression.NewBuilder().WithKeyCondition(condition) + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(GitLabOrgLowerNameIndex), + } + + log.WithFields(f).Debugf("querying for GitLab organization by name using organization_name_lower=%s...", strings.ToLower(gitLabOrganizationName)) + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving gitlab_organizations using gitLabOrganizationName = %s", gitLabOrganizationName) + return nil, err + } + if len(results.Items) == 0 { + log.WithFields(f).Debug("Unable to find GitLab organization by name - no results") + return nil, nil + } + + var resultOutput []*common.GitLabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem decoding database results, error: %+v", err) + return nil, err + } + + return resultOutput[0], nil +} + +// GetGitLabOrganizationByExternalID returns the GitLab Group/Org based on the external GitLab Group ID value +func (repo *Repository) GetGitLabOrganizationByExternalID(ctx context.Context, gitLabGroupID int64) (*common.GitLabOrganization, error) { + f := logrus.Fields{ + "functionName": "v1.gitlab_organizations.repository.GetGitLabOrganizationByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabGroupID": gitLabGroupID, + } + + condition := expression.Key(GitLabOrganizationsExternalGitLabGroupIDColumn).Equal(expression.Value(gitLabGroupID)) + builder := expression.NewBuilder().WithKeyCondition(condition) + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(GitLabExternalIDIndex), + } + + log.WithFields(f).Debugf("querying for GitLab organization by external group ID: %d...", gitLabGroupID) + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving gitlab_organizations using external ID = %d", gitLabGroupID) + return nil, err + } + if len(results.Items) == 0 { + log.WithFields(f).Debugf("Unable to find GitLab organization by group ID: %d - no results", gitLabGroupID) + return nil, nil + } + + var resultOutput []*common.GitLabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem decoding database results, error: %+v", err) + return nil, err + } + + return resultOutput[0], nil +} + +// GetGitLabOrganizationByURL loads the organization based on the url +func (repo *Repository) GetGitLabOrganizationByURL(ctx context.Context, url string) (*common.GitLabOrganization, error) { + f := logrus.Fields{ + "functionName": "v1.gitlab_organizations.repository. GetGitLabOrganizationByURL", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "URL": url, + } + + condition := expression.Key(GitLabOrganizationsOrganizationURLColumn).Equal(expression.Value(url)) + builder := expression.NewBuilder().WithKeyCondition(condition) + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(GitlabOrgURLIndex), + } + + log.WithFields(f).Debugf("querying for GitLab group by url: %s...", url) + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving GitLab group by url: %s", url) + return nil, err + } + if len(results.Items) == 0 { + log.WithFields(f).Debugf("Unable to find GitLab group by url: %s - no results", url) + return nil, nil + } + + var resultOutput []*common.GitLabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem decoding database results, error: %+v", err) + return nil, err + } + + return resultOutput[0], nil +} + +// GetGitLabOrganizationByFullPath loads the organization based on the full path value +func (repo *Repository) GetGitLabOrganizationByFullPath(ctx context.Context, groupFullPath string) (*common.GitLabOrganization, error) { + f := logrus.Fields{ + "functionName": "v1.gitlab_organizations.repository.GetGitLabOrganizationByFullPath", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "groupFullPath": groupFullPath, + } + + condition := expression.Key(GitLabOrganizationsOrganizationFullPathColumn).Equal(expression.Value(groupFullPath)) + builder := expression.NewBuilder().WithKeyCondition(condition) + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(GitLabFullPathIndex), + } + + log.WithFields(f).Debugf("querying for GitLab group by full path: %s...", groupFullPath) + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving GitLab group by full path: %s", groupFullPath) + return nil, err + } + if len(results.Items) == 0 { + log.WithFields(f).Debugf("Unable to find GitLab group by full path: %s - no results", groupFullPath) + return nil, nil + } + + var resultOutput []*common.GitLabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem decoding database results, error: %+v", err) + return nil, err + } + + return resultOutput[0], nil +} + +// GetGitLabOrganization by organization name +func (repo *Repository) GetGitLabOrganization(ctx context.Context, gitLabOrganizationID string) (*common.GitLabOrganization, error) { + f := logrus.Fields{ + "functionName": "gitlab_organizations.repository.GetGitLabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationID": gitLabOrganizationID, + } + + log.WithFields(f).Debugf("Querying for GitLab organization by ID: %s", gitLabOrganizationID) + result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + GitLabOrganizationsOrganizationIDColumn: { + S: aws.String(gitLabOrganizationID), + }, + }, + TableName: aws.String(repo.gitlabOrgTableName), + }) + if err != nil { + return nil, err + } + if len(result.Item) == 0 { + log.WithFields(f).Debugf("Unable to find GitLab organization by ID: %s - no results", gitLabOrganizationID) + return nil, nil + } + + var org common.GitLabOrganization + err = dynamodbattribute.UnmarshalMap(result.Item, &org) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling organization table data, error: %v2Models", err) + return nil, err + } + return &org, nil +} + +// UpdateGitLabOrganizationAuth updates the specified Gitlab organization oauth info +func (repo *Repository) UpdateGitLabOrganizationAuth(ctx context.Context, organizationID string, gitLabGroupID int, authExpiryTime int64, authInfo, groupName, groupFullPath, organizationURL string) error { + f := logrus.Fields{ + "functionName": "gitlab_organizations.repository.UpdateGitLabOrganizationAuth", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "organizationID": organizationID, + "groupName": groupName, + "groupFullPath": groupFullPath, + "organizationURL": organizationURL, + "tableName": repo.gitlabOrgTableName, + } + + _, currentTime := utils.CurrentTime() + gitlabOrg, lookupErr := repo.GetGitLabOrganization(ctx, organizationID) + if lookupErr != nil || gitlabOrg == nil { + log.WithFields(f).WithError(lookupErr).Warnf("error looking up Gitlab organization by id: %s, error: %+v", organizationID, lookupErr) + return lookupErr + } + + expressionAttributeNames := map[string]*string{ + "#A": aws.String(GitLabOrganizationsAuthInfoColumn), + "#U": aws.String(GitLabOrganizationsOrganizationURLColumn), + "#FP": aws.String(GitLabOrganizationsOrganizationFullPathColumn), + "#M": aws.String(GitLabOrganizationsDateModifiedColumn), + "#P": aws.String(GitLabOrganizationsExternalGitLabGroupIDColumn), + "#E": aws.String(GitLabOrganizationsAuthExpiryTimeColumn), + } + expressionAttributeValues := map[string]*dynamodb.AttributeValue{ + ":a": { + S: aws.String(authInfo), + }, + ":u": { + S: aws.String(organizationURL), + }, + ":fp": { + S: aws.String(groupFullPath), + }, + ":m": { + S: aws.String(currentTime), + }, + ":p": { + N: aws.String(strconv.Itoa(gitLabGroupID)), + }, + ":e": { + N: aws.String(strconv.FormatInt(authExpiryTime, 10)), + }, + } + updateExpression := "SET #A = :a, #U = :u, #FP = :fp, #M = :m, #P = :p, #E = :e" + + if groupName != "" { + expressionAttributeNames["#N"] = aws.String(GitLabOrganizationsOrganizationNameColumn) + expressionAttributeValues[":n"] = &dynamodb.AttributeValue{S: aws.String(groupName)} + updateExpression = fmt.Sprintf("%s, #N = :n ", updateExpression) + + expressionAttributeNames["#NL"] = aws.String(GitLabOrganizationsOrganizationNameLowerColumn) + expressionAttributeValues[":nl"] = &dynamodb.AttributeValue{S: aws.String(strings.ToLower(groupName))} + updateExpression = fmt.Sprintf("%s, #NL = :nl ", updateExpression) + } + + input := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + GitLabOrganizationsOrganizationIDColumn: { + S: aws.String(gitlabOrg.OrganizationID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(repo.gitlabOrgTableName), + } + + log.WithFields(f).Debug("updating gitlab organization record...") + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("unable to update Gitlab organization record, error: %+v", updateErr) + return updateErr + } + + return nil +} + +// UpdateGitLabOrganization updates the GitLab group based on the specified values +func (repo *Repository) UpdateGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization, enabled bool) error { + f := logrus.Fields{ + "functionName": "gitlab_organizations.repository.UpdateGitLabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": input.ProjectSFID, + "groupID": input.ExternalGroupID, + "groupFullPath": input.OrganizationFullPath, + "organizationName": input.OrganizationName, + "autoEnabled": input.AutoEnabled, + "autoEnabledClaGroupID": input.AutoEnabledClaGroupID, + "branchProtectionEnabled": input.BranchProtectionEnabled, + "enabled": enabled, + "tableName": repo.gitlabOrgTableName, + } + + var existingRecord *common.GitLabOrganization + var getErr error + if input.ExternalGroupID > 0 { + log.WithFields(f).Debugf("checking to see if we have an existing GitLab organization with ID: %d", input.ExternalGroupID) + existingRecord, getErr = repo.GetGitLabOrganizationByExternalID(ctx, input.ExternalGroupID) + if getErr != nil { + msg := fmt.Sprintf("unable to locate existing GitLab group by ID: %d, error: %+v", input.ExternalGroupID, input.OrganizationFullPath) + log.WithFields(f).WithError(getErr).Warn(msg) + return errors.New(msg) + } + } else if input.OrganizationFullPath != "" { + log.WithFields(f).Debugf("checking to see if we have an existing GitLab group full path with value: %s", input.OrganizationFullPath) + existingRecord, getErr = repo.GetGitLabOrganizationByFullPath(ctx, input.OrganizationFullPath) + if getErr != nil { + msg := fmt.Sprintf("unable to locate existing GitLab group by full path: %s, error: %+v", input.OrganizationFullPath, getErr) + log.WithFields(f).WithError(getErr).Warn(msg) + return errors.New(msg) + } + } + + if existingRecord == nil { + msg := fmt.Sprintf("error looking up GitLab group using group ID: %d or full path: %s - no results", input.ExternalGroupID, input.OrganizationFullPath) + log.WithFields(f).Warn(msg) + return errors.New(msg) + } + + _, currentTime := utils.CurrentTime() + note := fmt.Sprintf("Updated configuration on %s by %s.", currentTime, utils.GetUserNameFromContext(ctx)) + if existingRecord.Note != "" { + note = fmt.Sprintf("%s. %s", existingRecord.Note, note) + } + + expressionAttributeNames := map[string]*string{ + "#AE": aws.String(GitLabOrganizationsAutoEnabledColumn), + "#AECLA": aws.String(GitLabOrganizationsAutoEnabledCLAGroupIDColumn), + "#BP": aws.String(GitLabOrganizationsBranchProtectionEnabledColumn), + "#M": aws.String(GitLabOrganizationsDateModifiedColumn), + "#E": aws.String(GitLabOrganizationsEnabledColumn), + "#N": aws.String(GitLabOrganizationsNoteColumn), + } + expressionAttributeValues := map[string]*dynamodb.AttributeValue{ + ":ae": { + BOOL: aws.Bool(input.AutoEnabled), + }, + ":aecla": { + S: aws.String(input.AutoEnabledClaGroupID), + }, + ":bp": { + BOOL: aws.Bool(input.BranchProtectionEnabled), + }, + ":m": { + S: aws.String(currentTime), + }, + ":e": { + BOOL: aws.Bool(enabled), + }, + ":n": { + S: aws.String(note), + }, + } + updateExpression := "SET #AE = :ae, #AECLA = :aecla, #BP = :bp, #M = :m, #E = :e, #N = :n " + + if input.OrganizationName != "" { + expressionAttributeNames["#N"] = aws.String(GitLabOrganizationsOrganizationNameColumn) + expressionAttributeValues[":n"] = &dynamodb.AttributeValue{S: aws.String(input.OrganizationName)} + updateExpression = fmt.Sprintf("%s, #N = :n ", updateExpression) + + expressionAttributeNames["#NL"] = aws.String(GitLabOrganizationsOrganizationNameColumn) + expressionAttributeValues[":nl"] = &dynamodb.AttributeValue{S: aws.String(strings.ToLower(input.OrganizationName))} + updateExpression = fmt.Sprintf("%s, #NL = :nl ", updateExpression) + } + + updateItemInput := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + GitLabOrganizationsOrganizationIDColumn: { + S: aws.String(existingRecord.OrganizationID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(repo.gitlabOrgTableName), + } + + log.WithFields(f).Debugf("updating GitLab organization record: %+v", input) + _, updateErr := repo.dynamoDBClient.UpdateItem(updateItemInput) + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("unable to update GitLab organization record, error: %+v", updateErr) + return updateErr + } + + return nil +} + +// DeleteGitLabOrganizationByFullPath deletes the specified GitLab organization +func (repo *Repository) DeleteGitLabOrganizationByFullPath(ctx context.Context, projectSFID, gitlabOrgFullPath string) error { + f := logrus.Fields{ + "functionName": "v1.gitlab_organizations.repository.DeleteGitLabOrganizationByFullPath", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "gitlabOrgFullPath": gitlabOrgFullPath, + } + + log.WithFields(f).Debugf("loading GitLab group/organizations list for path: %s", gitlabOrgFullPath) + org, orgErr := repo.GetGitLabOrganizationByFullPath(ctx, gitlabOrgFullPath) + if orgErr != nil { + errMsg := fmt.Sprintf("GitLab group/organization is not found using group/organization: %s, error: %+v", gitlabOrgFullPath, orgErr) + log.WithFields(f).WithError(orgErr).Warn(errMsg) + return errors.New(errMsg) + } + // Nothing to delete or disable + if org == nil || !org.Enabled { + return nil + } + + log.WithFields(f).Debugf("deleting GitLab group/organization under path: %s...", gitlabOrgFullPath) + // Update enabled flag as false + _, currentTime := utils.CurrentTime() + note := fmt.Sprintf("Enabled set to false due to org deletion on %s by %s.", currentTime, utils.GetUserNameFromContext(ctx)) + if org.Note != "" { + note = fmt.Sprintf("%s. %s", org.Note, note) + } + _, err := repo.dynamoDBClient.UpdateItem( + &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + GitLabOrganizationsOrganizationIDColumn: { + S: aws.String(org.OrganizationID), + }, + }, + ExpressionAttributeNames: map[string]*string{ + "#E": aws.String(GitLabOrganizationsEnabledColumn), + "#N": aws.String(GitLabOrganizationsNoteColumn), + "#D": aws.String(GitLabOrganizationsDateModifiedColumn), + "#AI": aws.String(GitLabOrganizationsAuthInfoColumn), + "#AE": aws.String(GitLabOrganizationsAutoEnabledColumn), + "#AECLA": aws.String(GitLabOrganizationsAutoEnabledCLAGroupIDColumn), + "#EID": aws.String(GitLabOrganizationsExternalGitLabGroupIDColumn), + "#BP": aws.String(GitLabOrganizationsBranchProtectionEnabledColumn), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":e": { + BOOL: aws.Bool(false), + }, + ":n": { + S: aws.String(note), + }, + ":d": { + S: aws.String(currentTime), + }, + ":ai": { + S: aws.String(""), + }, + ":ae": { + BOOL: aws.Bool(false), + }, + ":aecla": { + S: aws.String(""), + }, + ":eid": { + N: aws.String("0"), + }, + ":bp": { + BOOL: aws.Bool(false), + }, + }, + UpdateExpression: aws.String("SET #E = :e, #N = :n, #D = :d, #AI = :ai, #AE = :ae, #AECLA = :aecla, #EID = :eid, #BP = :bp"), + TableName: aws.String(repo.gitlabOrgTableName), + }, + ) + if err != nil { + errMsg := fmt.Sprintf("error updating gitlab organization by path: %s using GitLab group/organization ID: %s - %+v", gitlabOrgFullPath, org.OrganizationID, err) + log.WithFields(f).WithError(err).Warnf(errMsg) + return errors.New(errMsg) + } + + return nil +} + +func buildGitlabOrganizationListModels(ctx context.Context, gitlabOrganizations []*common.GitLabOrganization) []*v2Models.GitlabOrganization { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.buildGitlabOrganizationListModels", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + log.WithFields(f).Debugf("fetching gitlab info for the list") + // Convert the database model to a response model + return common.ToModels(gitlabOrganizations) +} + +// getOrganizationsWithConditionFilter fetches the repository entry based on the specified condition and filter criteria +// using the provided index +func (repo *Repository) getOrganizationsWithConditionFilter(ctx context.Context, condition expression.KeyConditionBuilder, filter expression.ConditionBuilder, indexName string) (*v2Models.GitlabOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.getOrganizationsWithConditionFilter", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "indexName": indexName, + } + + builder := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem building query expression, error: %+v", err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(indexName), + } + + log.WithFields(f).Debugf("query: %+v", queryInput) + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).Warnf("problem retrieving gitlab_organizations, error = %s", err.Error()) + return nil, err + } + + if len(results.Items) == 0 { + log.WithFields(f).Debug("no results from query") + return &v2Models.GitlabOrganizations{ + List: []*v2Models.GitlabOrganization{}, + }, nil + } + + var resultOutput []*common.GitLabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + return nil, err + } + + log.WithFields(f).Debugf("building response model for %d results...", len(resultOutput)) + gitlabOrgList := buildGitlabOrganizationListModels(ctx, resultOutput) + return &v2Models.GitlabOrganizations{List: gitlabOrgList}, nil +} + +func updateResponse(fullResponse, response *v2Models.GitlabOrganizations) { + if fullResponse.List == nil { + fullResponse.List = response.List + return + } + + if response != nil && response.List != nil { + for _, item := range response.List { + found := false + for _, fr := range fullResponse.List { + if fr.OrganizationID == item.OrganizationID { + found = true + break + } + } + if !found { + fullResponse.List = append(fullResponse.List, item) + } + } + } +} + +func (repo *Repository) getScanResults(ctx context.Context, filter *expression.ConditionBuilder) (*v2Models.GitlabOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.GetGitLabOrganizations", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + builder := expression.NewBuilder() + + // Add the filter if provided + if filter != nil { + builder = builder.WithFilter(*filter) + } + + // Build the scan/query expression + expr, builderErr := builder.Build() + if builderErr != nil { + log.WithFields(f).Warnf("error building expression for %s scan, error: %v", repo.gitlabOrgTableName, builderErr) + return nil, builderErr + } + + scanInput := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.gitlabOrgTableName), + } + + var resultList []map[string]*dynamodb.AttributeValue + for { + results, scanErr := repo.dynamoDBClient.Scan(scanInput) //nolint + if scanErr != nil { + log.WithFields(f).Warnf("error retrieving scan results from table %s, error: %v", repo.gitlabOrgTableName, scanErr) + return nil, scanErr + } + resultList = append(resultList, results.Items...) + if len(results.LastEvaluatedKey) != 0 { + scanInput.ExclusiveStartKey = results.LastEvaluatedKey + } else { + break + } + } + + var resultOutput []*common.GitLabOrganization + unmarshalErr := dynamodbattribute.UnmarshalListOfMaps(resultList, &resultOutput) + if unmarshalErr != nil { + log.Warnf("error unmarshalling %s from database. error: %v", repo.gitlabOrgTableName, unmarshalErr) + return nil, unmarshalErr + } + + return &v2Models.GitlabOrganizations{List: common.ToModels(resultOutput)}, nil +} diff --git a/cla-backend-go/v2/gitlab_organizations/service.go b/cla-backend-go/v2/gitlab_organizations/service.go new file mode 100644 index 000000000..160274217 --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/service.go @@ -0,0 +1,1060 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/communitybridge/easycla/cla-backend-go/company" + "github.com/communitybridge/easycla/cla-backend-go/signatures" + "github.com/communitybridge/easycla/cla-backend-go/users" + "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" + + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + "github.com/communitybridge/easycla/cla-backend-go/config" + gitlabApi "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "github.com/go-openapi/strfmt" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + projectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + + "github.com/communitybridge/easycla/cla-backend-go/events" + 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/projects_cla_groups" + "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/v2/store" + "github.com/sirupsen/logrus" + goGitLab "github.com/xanzy/go-gitlab" +) + +// ServiceInterface contains functions of GitlabOrganizations service +type ServiceInterface interface { + AddGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization) (*v2Models.GitlabProjectOrganizations, error) + GetGitLabOrganization(ctx context.Context, gitLabOrganizationID string) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizationByID(ctx context.Context, gitLabOrganizationID string) (*common.GitLabOrganization, error) + GetGitLabOrganizationByName(ctx context.Context, gitLabOrganizationName string) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizationByFullPath(ctx context.Context, gitLabOrganizationFullPath string) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizationByURL(ctx context.Context, url string) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizationByGroupID(ctx context.Context, gitLabGroupID int64) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizations(ctx context.Context) (*v2Models.GitlabProjectOrganizations, error) + GetGitLabOrganizationsEnabled(ctx context.Context) (*v2Models.GitlabProjectOrganizations, error) + GetGitLabOrganizationsEnabledWithAutoEnabled(ctx context.Context) (*v2Models.GitlabProjectOrganizations, error) + GetGitLabOrganizationsByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabProjectOrganizations, error) + GetGitLabOrganizationByState(ctx context.Context, gitLabOrganizationID, authState string) (*v2Models.GitlabOrganization, error) + GetGitLabGroupMembers(ctx context.Context, groupID string) (*v2Models.GitlabGroupMembersList, error) + UpdateGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization) error + UpdateGitLabOrganizationAuth(ctx context.Context, gitLabOrganizationID string, oauthResp *gitlabApi.OauthSuccessResponse, authExpiryTime int64) error + DeleteGitLabOrganizationByFullPath(ctx context.Context, projectSFID string, gitlabOrgFullPath string) error + InitiateSignRequest(ctx context.Context, req *http.Request, gitlabClient *goGitLab.Client, repositoryID, mergeRequestID, originURL, contributorBaseURL string, eventService events.Service) (*string, error) + RefreshGitLabOrganizationAuth(ctx context.Context, gitLabOrg *common.GitLabOrganization) (*string, error) +} + +// Service data modelffGetGitLabOrganizationByID +type Service struct { + repo RepositoryInterface + v2GitRepoService repositories.ServiceInterface + claGroupRepository projects_cla_groups.Repository + gitLabApp *gitlabApi.App + storeRepo store.Repository + userService users.Service + signatureRepo signatures.SignatureRepository + companyRepository company.IRepository +} + +// NewService creates a new gitlab organization service +func NewService(repo RepositoryInterface, v2GitRepoService repositories.ServiceInterface, claGroupRepository projects_cla_groups.Repository, storeRepo store.Repository, userService users.Service, signaturesRepo signatures.SignatureRepository, companyRepository company.IRepository) ServiceInterface { + return &Service{ + repo: repo, + v2GitRepoService: v2GitRepoService, + claGroupRepository: claGroupRepository, + gitLabApp: gitlabApi.Init(config.GetConfig().Gitlab.AppClientID, config.GetConfig().Gitlab.AppClientSecret, config.GetConfig().Gitlab.AppPrivateKey), + userService: userService, + storeRepo: storeRepo, + signatureRepo: signaturesRepo, + companyRepository: companyRepository, + } +} + +// AddGitLabOrganization adds the specified GitLab organization +func (s *Service) AddGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization) (*v2Models.GitlabProjectOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.AddGitLabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": input.ProjectSFID, + "parentProjectSFID": input.ParentProjectSFID, + "autoEnabled": input.AutoEnabled, + "branchProtectionEnabled": input.BranchProtectionEnabled, + "groupID": input.ExternalGroupID, + "groupFullPath": input.OrganizationFullPath, + } + + var existingModel *v2Models.GitlabOrganization + var getErr error + if input.OrganizationFullPath != "" { + existingModel, getErr = s.GetGitLabOrganizationByFullPath(ctx, input.OrganizationFullPath) + if getErr != nil { + log.WithFields(f).WithError(getErr).Warnf("problem querying GitLab group/organization using full path: %s", input.OrganizationFullPath) + return nil, getErr + } + } + if input.ExternalGroupID > 0 { + existingModel, getErr = s.GetGitLabOrganizationByGroupID(ctx, input.ExternalGroupID) + if getErr != nil { + log.WithFields(f).WithError(getErr).Warnf("problem querying GitLab group/organization using group ID: %d", input.ExternalGroupID) + return nil, getErr + } + } + + // If we have an existing record/entry + if existingModel != nil { + // Check to make sure another project doesn't own this GitLab Group - only care about conflicts if it is enabled + if existingModel.ProjectSfid != input.ProjectSFID && existingModel.Enabled { + psc := projectService.GetClient() + requestedProjectModel, projectLookupErr := psc.GetProject(input.ProjectSFID) + if projectLookupErr != nil || requestedProjectModel == nil { + return nil, projectLookupErr + } + existingProjectModel, projectLookupErr := psc.GetProject(existingModel.ProjectSfid) + if projectLookupErr != nil || existingProjectModel == nil { + log.WithFields(f).WithError(projectLookupErr).Warnf("unable to lookup project with SFID: %s", existingModel.ProjectSfid) + return nil, projectLookupErr + } + msg := fmt.Sprintf("unable to add or update the GitLab Group/Organization - already taken by another project: %s (%s) - unable to add to this project: %s (%s)", + existingProjectModel.Name, existingModel.ProjectSfid, + requestedProjectModel.Name, input.ProjectSFID) + log.WithFields(f).Warn(msg) + + // Return the error model + return nil, &utils.ProjectConflict{ + Message: "unable to add or update the GitLab Group/Organization - already taken by another project", + ProjectA: utils.ProjectSummary{ + Name: requestedProjectModel.Name, + ID: input.ProjectSFID, + }, + ProjectB: utils.ProjectSummary{ + Name: existingProjectModel.Name, + ID: existingModel.ProjectSfid, + }, + } + } + + updateErr := s.UpdateGitLabOrganization(ctx, input) + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("problem updating GitLab group/organization, error: %+v", updateErr) + return nil, getErr + } + return s.GetGitLabOrganizationsByProjectSFID(ctx, input.ProjectSFID) + } + + log.WithFields(f).Debug("adding GitLab organization...") + resp, err := s.repo.AddGitLabOrganization(ctx, input, true) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem adding gitlab organization for project") + return nil, err + } + log.WithFields(f).Debugf("created GitLab organization with ID: %s", resp.OrganizationID) + + return s.GetGitLabOrganizationsByProjectSFID(ctx, input.ProjectSFID) +} + +// GetGitLabOrganization returns the GitLab organization based on the specified GitLab Organization ID +func (s *Service) GetGitLabOrganization(ctx context.Context, gitlabOrganizationID string) (*v2Models.GitlabOrganization, error) { + dbModel, err := s.GetGitLabOrganizationByID(ctx, gitlabOrganizationID) + if err != nil { + return nil, err + } + + if dbModel == nil { + return nil, nil + } + + return common.ToModel(dbModel), err +} + +// GetGitLabOrganizationByID returns the record associated with the GitLab Organization ID +func (s *Service) GetGitLabOrganizationByID(ctx context.Context, gitLabOrganizationID string) (*common.GitLabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganizationByID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationID": gitLabOrganizationID, + } + + log.WithFields(f).Debugf("fetching gitlab organization for gitlab org id: %s", gitLabOrganizationID) + dbModel, err := s.repo.GetGitLabOrganization(ctx, gitLabOrganizationID) + if err != nil { + return nil, err + } + + return dbModel, nil +} + +// GetGitLabOrganizationByName returns the gitlab organization based on the Group/Org name +func (s *Service) GetGitLabOrganizationByName(ctx context.Context, gitLabOrganizationName string) (*v2Models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganizationByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabOrganizationID": gitLabOrganizationName, + } + + log.WithFields(f).Debugf("fetching gitlab organization for gitlab org id: %s", gitLabOrganizationName) + dbModel, err := s.repo.GetGitLabOrganizationByName(ctx, gitLabOrganizationName) + if err != nil { + return nil, err + } + + return common.ToModel(dbModel), nil +} + +// GetGitLabOrganizationByFullPath returns the GitLab group/organization using the specified full path +func (s *Service) GetGitLabOrganizationByFullPath(ctx context.Context, gitLabOrganizationFullPath string) (*v2Models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganizationByFullPath", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationFullPath": gitLabOrganizationFullPath, + } + + log.WithFields(f).Debugf("fetching gitlab group/organization using full path: %s", gitLabOrganizationFullPath) + dbModel, err := s.repo.GetGitLabOrganizationByFullPath(ctx, gitLabOrganizationFullPath) + if err != nil { + return nil, err + } + + if dbModel == nil { + return nil, nil + } + return common.ToModel(dbModel), nil +} + +// GetGitLabOrganizationByURL returns the GitLab group/organization using the specified full path +func (s *Service) GetGitLabOrganizationByURL(ctx context.Context, URL string) (*v2Models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganizationByURL", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationFullPath": URL, + } + + log.WithFields(f).Debugf("fetching gitlab group/organization using url: %s", URL) + dbModel, err := s.repo.GetGitLabOrganizationByURL(ctx, URL) + if err != nil { + return nil, err + } + + if dbModel == nil { + return nil, nil + } + return common.ToModel(dbModel), nil +} + +// GetGitLabOrganizationByGroupID returns the GitLab group/organization using the specified group ID +func (s *Service) GetGitLabOrganizationByGroupID(ctx context.Context, gitLabGroupID int64) (*v2Models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganizationByGroupID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabGroupID": gitLabGroupID, + } + + log.WithFields(f).Debugf("fetching gitlab group/organization using group ID: %d", gitLabGroupID) + dbModel, err := s.repo.GetGitLabOrganizationByExternalID(ctx, gitLabGroupID) + if err != nil { + return nil, err + } + + if dbModel == nil { + return nil, nil + } + + return common.ToModel(dbModel), nil +} + +// GetGitLabOrganizations returns the complete list of GitLab groups/organizations +func (s *Service) GetGitLabOrganizations(ctx context.Context) (*v2Models.GitlabProjectOrganizations, error) { + gitLabOrganizations, err := s.repo.GetGitLabOrganizations(ctx) + if err != nil { + return nil, err + } + + // Our response model + out := &v2Models.GitlabProjectOrganizations{ + List: s.toGitLabProjectOrganizationList(ctx, gitLabOrganizations), + } + + return out, nil +} + +// GetGitLabOrganizationsEnabled returns the list of GitLab groups/organizations that are enabled +func (s *Service) GetGitLabOrganizationsEnabled(ctx context.Context) (*v2Models.GitlabProjectOrganizations, error) { + gitLabOrganizations, err := s.repo.GetGitLabOrganizationsEnabled(ctx) + if err != nil { + return nil, err + } + + // Our response model + out := &v2Models.GitlabProjectOrganizations{ + List: s.toGitLabProjectOrganizationList(ctx, gitLabOrganizations), + } + + return out, nil +} + +// GetGitLabOrganizationsEnabledWithAutoEnabled returns the list of GitLab groups/organizations that are enabled with the auto enabled flag set to true +func (s *Service) GetGitLabOrganizationsEnabledWithAutoEnabled(ctx context.Context) (*v2Models.GitlabProjectOrganizations, error) { + gitLabOrganizations, err := s.repo.GetGitLabOrganizationsEnabledWithAutoEnabled(ctx) + if err != nil { + return nil, err + } + + // Our response model + out := &v2Models.GitlabProjectOrganizations{ + List: s.toGitLabProjectOrganizationList(ctx, gitLabOrganizations), + } + + return out, nil +} + +// GetGitLabGroupMembers gets group members +func (s *Service) GetGitLabGroupMembers(ctx context.Context, groupID string) (*v2Models.GitlabGroupMembersList, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabGroupMembers", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "groupID": groupID, + } + groupMemberList := make([]*v2Models.GitlabGroupMember, 0) + gitlabOrg, err := s.GetGitLabOrganization(ctx, groupID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to fetch gitlab details") + return nil, err + } + + if gitlabOrg != nil { + oauthResponse, err := s.RefreshGitLabOrganizationAuth(ctx, common.ToCommonModel(gitlabOrg)) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to refresh gitlab auth") + return nil, err + } + glClient, clientErr := gitlabApi.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if clientErr != nil { + log.WithFields(f).WithError(clientErr).Warn("problem getting gitLabClient") + return nil, clientErr + } + + members, err := gitlabApi.ListGroupMembers(ctx, glClient, int(gitlabOrg.OrganizationExternalID)) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to get group members list") + return nil, err + } + + if len(members) > 0 { + for _, member := range members { + groupMemberList = append(groupMemberList, &v2Models.GitlabGroupMember{ + Name: member.Name, + ID: strconv.Itoa(member.ID), + Username: member.Username, + }) + } + } + + } + + log.WithFields(f).Debugf("Members: %+v ", groupMemberList) + + return &v2Models.GitlabGroupMembersList{ + List: groupMemberList, + }, nil +} + +// GetGitLabOrganizationsByProjectSFID returns a collection of GitLab organizations based on the specified project SFID value +func (s *Service) GetGitLabOrganizationsByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabProjectOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganizationsByProjectSFID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + } + + psc := v2ProjectService.GetClient() + log.WithFields(f).Debug("loading project details from the project service...") + projectServiceRecord, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading project details from the project service") + return nil, err + } + + var parentProjectSFID string + if utils.IsProjectHasRootParent(projectServiceRecord) { + parentProjectSFID = projectSFID + } else { + parentProjectSFID = utils.StringValue(projectServiceRecord.Parent) + } + f["parentProjectSFID"] = parentProjectSFID + log.WithFields(f).Debug("located parentProjectID...") + + // Load the GitLab Organization and Repository details - result will be missing CLA Group info and ProjectSFID details + pcg, pcgErr := s.claGroupRepository.GetClaGroupIDForProject(ctx, projectSFID) + if err != 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 + } + } + + orgList := &v2Models.GitlabOrganizations{ + List: make([]*v2Models.GitlabOrganization, 0), + } + + if pcg != nil && pcg.FoundationSFID != "" { + log.WithFields(f).Debugf("loading Gitlab organizations for foundationSFID: %s", pcg.FoundationSFID) + orgList, err = s.repo.GetGitLabOrganizationsByFoundationSFID(ctx, pcg.FoundationSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading gitlab organizations from the project service") + return nil, err + } + log.WithFields(f).Debugf("loaded %d Gitlab organizations for foundationSFID: %s", len(orgList.List), pcg.FoundationSFID) + } else { + log.WithFields(f).Debugf("loading Gitlab organizations for projectSFID: %s", projectSFID) + orgList, err = s.repo.GetGitLabOrganizationsByProjectSFID(ctx, projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading gitlab organizations from the project service") + return nil, err + } + log.WithFields(f).Debugf("loaded %d Gitlab organizations for projectSFID: %s", len(orgList.List), projectSFID) + } + + log.WithFields(f).Debugf("GitLab Organizations: %+v ", orgList) + + // Our response model + out := &v2Models.GitlabProjectOrganizations{ + List: s.toGitLabProjectOrganizationList(ctx, orgList), + } + + return out, nil +} + +// RefreshGitLabOrganizationAuth refreshes the GitLab organization auth token in case of expired token +func (s *Service) RefreshGitLabOrganizationAuth(ctx context.Context, gitLabOrg *common.GitLabOrganization) (*string, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.RefreshGitLabOrganizationAuth", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + var gitLabAuthResponse string + var err error + + // decrypt oauthResponse + decryptedOauthResponse, err := gitlabApi.DecryptAuthInfo(gitLabOrg.AuthInfo, s.gitLabApp) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem decrypting oauthResponse") + return nil, err + } + + log.WithFields(f).Debugf("AuthExpirationTime: %d", gitLabOrg.AuthExpirationTime) + expireTime := time.Unix(int64(gitLabOrg.AuthExpirationTime), 0) + log.WithFields(f).Debugf("expiring time: %+v, current time: %v", utils.TimeToString(expireTime), utils.TimeToString(time.Now())) + + timeBuffer := 30 * time.Second + + // If the current time (minus a small buffer/window) is AFTER the expiration time, refresh the token + if gitLabOrg.AuthExpirationTime == 0 || time.Now().Add(timeBuffer).After(expireTime) { + log.WithFields(f).Debugf("refreshing gitlab auth token - now + buffer: %v - expiration: %v", time.Now().Add(timeBuffer), expireTime) + refreshOauthResponse, err := gitlabApi.RefreshOauthToken(decryptedOauthResponse.RefreshToken) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem refreshing token") + return nil, err + } + log.WithFields(f).Debugf("refreshed oauthResponse: %+v - expiration in: %d seconds", refreshOauthResponse, refreshOauthResponse.ExpiresIn) + + // convert the expiration as number of seconds to a unix timestamp + authExpiryTime := time.Now().Add(time.Duration(refreshOauthResponse.ExpiresIn) * time.Second).Unix() + + // encrypt oauthResponse + gitLabAuthResponse, err = gitlabApi.EncryptAuthInfo(refreshOauthResponse, s.gitLabApp) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem encrypting oauthResponse") + return nil, err + } + + log.WithFields(f).Debug("encrypted oauthResponse: ", gitLabAuthResponse) + + err = s.repo.UpdateGitLabOrganizationAuth(ctx, gitLabOrg.OrganizationID, int(gitLabOrg.ExternalGroupID), authExpiryTime, gitLabAuthResponse, gitLabOrg.OrganizationName, gitLabOrg.OrganizationFullPath, gitLabOrg.OrganizationURL) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem updating gitlab organization auth") + return nil, err + } + } else { + log.WithFields(f).Debug("using existing gitlab auth token") + gitLabAuthResponse = gitLabOrg.AuthInfo + } + + return &gitLabAuthResponse, nil +} + +func (s *Service) toGitLabProjectOrganizationList(ctx context.Context, dbModels *v2Models.GitlabOrganizations) []*v2Models.GitlabProjectOrganization { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.toGitLabProjectOrganizationList", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + var response []*v2Models.GitlabProjectOrganization + log.WithFields(f).Debugf("converting %d GitLab organizations to response model", len(dbModels.List)) + + orgMap := make(map[string]*v2Models.GitlabProjectOrganization) + for _, org := range dbModels.List { + autoEnabledCLAGroupName := "" + if org.AutoEnabledClaGroupID != "" { + log.WithFields(f).Debugf("loading CLA Group by ID: %s to obtain the name for GitLab auth enabled CLA Group response", org.AutoEnabledClaGroupID) + claGroupMode, claGroupLookupErr := s.claGroupRepository.GetCLAGroup(ctx, org.AutoEnabledClaGroupID) + if claGroupLookupErr != nil { + log.WithFields(f).WithError(claGroupLookupErr).Warnf("Unable to lookup CLA Group by ID: %s", org.AutoEnabledClaGroupID) + } + if claGroupMode != nil { + autoEnabledCLAGroupName = claGroupMode.ProjectName + } + } + + log.WithFields(f).Debugf("loading GitLab organization by organization ID: %s", org.OrganizationID) + orgDetailed, orgErr := s.repo.GetGitLabOrganization(ctx, org.OrganizationID) + if orgErr != nil { + log.WithFields(f).Errorf("fetching gitlab org failed : %s : %v", org.OrganizationID, orgErr) + continue + } + + log.WithFields(f).Debugf("filtering repositories based on group path: %s", org.OrganizationFullPath) + repoList, repoErr := s.v2GitRepoService.GitLabGetRepositoriesByNamePrefix(ctx, fmt.Sprintf("%s/", org.OrganizationFullPath)) + if repoErr != nil { + if _, ok := repoErr.(*utils.GitLabRepositoryNotFound); ok { + log.WithFields(f).Debugf("no GitLab repositories onboarded for group/organization : %s", org.OrganizationFullPath) + repoErr = nil // ignore error + } else { + log.WithFields(f).WithError(repoErr).Debugf("unexpected error while fetching GitLab group repositories for group/organization path: %s, error type: %T, error: %v", org.OrganizationFullPath, repoErr, repoErr) + } + } + + rorg := &v2Models.GitlabProjectOrganization{ + AutoEnabled: org.AutoEnabled, + AutoEnableClaGroupID: org.AutoEnabledClaGroupID, + AutoEnabledClaGroupName: strings.TrimSpace(autoEnabledCLAGroupName), + ProjectSfid: org.ProjectSfid, + ParentProjectSfid: org.OrganizationSfid, + OrganizationName: org.OrganizationName, + OrganizationURL: org.OrganizationURL, + OrganizationFullPath: org.OrganizationFullPath, + OrganizationExternalID: org.OrganizationExternalID, + InstallationURL: buildInstallationURL(org.OrganizationID, orgDetailed.AuthState), + BranchProtectionEnabled: org.BranchProtectionEnabled, + ConnectionStatus: "", // updated below + Repositories: []*v2Models.GitlabProjectRepository{}, // updated below + } + + if orgDetailed.AuthInfo == "" { + rorg.ConnectionStatus = utils.NoConnection + } else { + if repoErr != nil { + log.WithFields(f).Warnf("initializing gitlab client for gitlab org: %s failed : %v", org.OrganizationID, repoErr) + rorg.ConnectionStatus = utils.ConnectionFailure + rorg.ConnectionStatusMessage = repoErr.Error() + } else { + // We've been authenticated by the user - great, see if we can determine the list of repos... + oauthResponse, err := s.RefreshGitLabOrganizationAuth(ctx, orgDetailed) + if err != nil { + log.WithFields(f).Warnf("refreshing gitlab auth for gitlab org: %s failed : %v", org.OrganizationID, err) + rorg.ConnectionStatus = utils.ConnectionFailure + rorg.ConnectionStatusMessage = err.Error() + oauthResponse = &orgDetailed.AuthInfo + } else { + rorg.ConnectionStatus = utils.Connected + rorg.ConnectionStatusMessage = "Connected" + } + + glClient, clientErr := gitlabApi.NewGitlabOauthClient(*oauthResponse, s.gitLabApp) + if clientErr != nil { + log.WithFields(f).Warnf("using gitlab client for gitlab group id: %d, internal group/org ID: %s failed: %v", org.OrganizationExternalID, org.OrganizationID, clientErr) + rorg.ConnectionStatus = utils.ConnectionFailure + rorg.ConnectionStatusMessage = clientErr.Error() + } else { + rorg.Repositories = s.updateRepositoryStatus(glClient, toGitLabProjectResponse(repoList)) + + user, _, userErr := glClient.Users.CurrentUser() + if userErr != nil { + log.WithFields(f).Warnf("using gitlab client for gitlab org: %s failed : %v", org.OrganizationID, userErr) + rorg.ConnectionStatus = utils.ConnectionFailure + rorg.ConnectionStatusMessage = userErr.Error() + } else { + log.WithFields(f).Debugf("connected to user : %s for gitlab org : %s", user.Name, org.OrganizationID) + rorg.ConnectionStatus = utils.Connected + } + } + } + } + + orgMap[org.OrganizationName] = rorg + response = append(response, rorg) + } + + // Sort everything nicely + sort.Slice(response, func(i, j int) bool { + return strings.ToLower(response[i].OrganizationName) < strings.ToLower(response[j].OrganizationName) + }) + for _, projectOrganization := range response { + sort.Slice(projectOrganization.Repositories, func(i, j int) bool { + return strings.ToLower(projectOrganization.Repositories[i].RepositoryName) < strings.ToLower(projectOrganization.Repositories[j].RepositoryName) + }) + } + + return response +} + +// GetGitLabOrganizationByState returns the GitLab organization by the auth state +func (s *Service) GetGitLabOrganizationByState(ctx context.Context, gitLabOrganizationID, authState string) (*v2Models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitLabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationID": gitLabOrganizationID, + "authState": authState, + } + + log.WithFields(f).Debugf("fetching gitlab organization for gitlab org id : %s", gitLabOrganizationID) + dbModel, err := s.repo.GetGitLabOrganization(ctx, gitLabOrganizationID) + if err != nil { + return nil, err + } + + if dbModel.AuthState != authState { + return nil, fmt.Errorf("auth state doesn't match") + } + + return common.ToModel(dbModel), nil +} + +// UpdateGitLabOrganizationAuth updates the GitLab organization authentication information +func (s *Service) UpdateGitLabOrganizationAuth(ctx context.Context, gitLabOrganizationID string, oauthResp *gitlabApi.OauthSuccessResponse, authExpiryTime int64) error { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.UpdateGitLabOrganizationAuth", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabOrganizationID": gitLabOrganizationID, + } + + log.WithFields(f).Debugf("updating gitlab org auth") + authInfoEncrypted, err := gitlabApi.EncryptAuthInfo(oauthResp, s.gitLabApp) + if err != nil { + return fmt.Errorf("encrypt failed : %v", err) + } + + gitLabOrgModel, err := s.GetGitLabOrganizationByID(ctx, gitLabOrganizationID) + if err != nil { + return fmt.Errorf("gitlab organization lookup error: %+v", err) + } + + // Get a reference to the GitLab client + gitLabClient, err := gitlabApi.NewGitlabOauthClientFromAccessToken(oauthResp.AccessToken) + if err != nil { + return fmt.Errorf("initializing gitlab client : %v", err) + } + + // Query the groups list + groupsWithMaintainerPerms, groupListErr := gitlabApi.GetGroupsListAll(ctx, gitLabClient, goGitLab.MaintainerPermissions) + if groupListErr != nil { + return groupListErr + } + + for _, g := range groupsWithMaintainerPerms { + // If we have an external group ID or a full path... + if (gitLabOrgModel.ExternalGroupID > 0 && g.ID == gitLabOrgModel.ExternalGroupID) || + (gitLabOrgModel.OrganizationFullPath != "" && g.FullPath == gitLabOrgModel.OrganizationFullPath) { + + updateGitLabOrgErr := s.repo.UpdateGitLabOrganizationAuth(ctx, gitLabOrganizationID, g.ID, authExpiryTime, authInfoEncrypted, g.Name, g.FullPath, g.WebURL) + if updateGitLabOrgErr != nil { + return updateGitLabOrgErr + } + + log.WithFields(f).Debugf("fetching updated GitLab group/organization record which should now have all the details") + updatedOrgDBModel, getErr := s.repo.GetGitLabOrganization(ctx, gitLabOrganizationID) + if getErr != nil { + return getErr + } + + log.WithFields(f).Debugf("adding GitLab repositories for this group/organization") + _, err = s.v2GitRepoService.GitLabAddRepositoriesByApp(ctx, updatedOrgDBModel) + if err != nil { + return err + } + + return nil + } + } + + msg := "" + if gitLabOrgModel.ExternalGroupID > 0 { + msg = fmt.Sprintf("external ID: %d", gitLabOrgModel.ExternalGroupID) + } else if gitLabOrgModel.OrganizationFullPath != "" { + msg = fmt.Sprintf("full path: '%s'", gitLabOrgModel.OrganizationFullPath) + } + + return fmt.Errorf("unable to locate the provided GitLab group by %s using the provided permissions - discovered %d groups where user has maintainer or above permissions", + msg, len(groupsWithMaintainerPerms)) +} + +// UpdateGitLabOrganization updates the GitLab organization +func (s *Service) UpdateGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization) error { + // check if valid cla group id is passed + if input.AutoEnabledClaGroupID != "" { + if _, err := s.claGroupRepository.GetCLAGroupNameByID(ctx, input.AutoEnabledClaGroupID); err != nil { + return err + } + } + + return s.repo.UpdateGitLabOrganization(ctx, input, true) +} + +// DeleteGitLabOrganizationByFullPath deletes the specified GitLab organization by full path +func (s *Service) DeleteGitLabOrganizationByFullPath(ctx context.Context, projectSFID string, gitLabOrgFullPath string) error { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.DeleteGitLabOrganizationByFullPath", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "gitLabOrgFullPath": gitLabOrgFullPath, + } + + // Check for enabled repos... + repoList, getRepListErr := s.v2GitRepoService.GitLabGetRepositoriesByProjectSFID(ctx, projectSFID) + if getRepListErr != nil { + // If nothing to delete... + if _, ok := getRepListErr.(*utils.GitLabRepositoryNotFound); ok { + log.WithFields(f).Debugf("no repositories found under GitLab group/organization: %s", gitLabOrgFullPath) + } else { + return getRepListErr + } + } + + // Check to see if we still have enabled repos belonging to this GitLab organization/group + var enabledRepoList []string + if repoList != nil && len(repoList.List) > 0 { + for _, repo := range repoList.List { + if strings.HasPrefix(repo.RepositoryName, gitLabOrgFullPath) && repo.Enabled { + enabledRepoList = append(enabledRepoList, repo.RepositoryName) + } + } + } + + if len(enabledRepoList) > 0 { + return fmt.Errorf("the following repositories are still enabled under the GitLab Group/Organization: %s - %s", gitLabOrgFullPath, strings.Join(enabledRepoList, ",")) + } + + // First delete the GitLab project/repos + log.WithFields(f).Debugf("deleting GitLab repos under group: %s", gitLabOrgFullPath) + repoDeleteErr := s.v2GitRepoService.GitLabDeleteRepositories(ctx, gitLabOrgFullPath) + if repoDeleteErr != nil { + log.WithFields(f).WithError(repoDeleteErr).Warnf("problem deleting GitLab repos under group: %s", gitLabOrgFullPath) + return repoDeleteErr + } + + return s.repo.DeleteGitLabOrganizationByFullPath(ctx, projectSFID, gitLabOrgFullPath) +} + +// InitiateSignRequest initiates sign request and returns easy cla redirect url +func (s *Service) InitiateSignRequest(ctx context.Context, req *http.Request, gitlabClient *goGitLab.Client, repositoryID, mergeRequestID, originURL, contributorBaseURL string, eventService events.Service) (*string, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.InitiateSignRequest", + "repositoryID": repositoryID, + "mergeRequestID": mergeRequestID, + "originURL": originURL, + } + + claUser, err := s.getOrCreateUser(ctx, gitlabClient, eventService) + if err != nil { + msg := fmt.Sprintf("unable to get or create user : %+v ", err) + log.WithFields(f).Warn(msg) + return nil, err + } + + repoIDInt, err := strconv.Atoi(repositoryID) + if err != nil { + msg := fmt.Sprintf("unable to convert GitlabRepoID: %s to int", repositoryID) + log.WithFields(f).Warn(msg) + return nil, err + } + + log.WithFields(f).Debugf("getting gitlab repository for: %d", repoIDInt) + gitlabRepo, err := s.v2GitRepoService.GitLabGetRepositoryByExternalID(ctx, int64(repoIDInt)) + if err != nil { + msg := fmt.Sprintf("unable to find repository by ID: %s , error: %+v ", repositoryID, err) + log.WithFields(f).Warn(msg) + return nil, err + } + + type StoreValue struct { + UserID string `json:"user_id"` + ProjectID string `json:"project_id"` + RepositoryID string `json:"repository_id"` + MergeRequestID string `json:"merge_request_id"` + ReturnURL string `json:"return_url"` + } + + log.WithFields(f).Debugf("setting active signature metadata: claUser: %+v, repository: %+v", claUser, gitlabRepo) + // set active signature metadata to track the user signing process + key := fmt.Sprintf("active_signature:%s", claUser.UserID) + storeValue := StoreValue{ + UserID: claUser.UserID, + ProjectID: gitlabRepo.RepositoryClaGroupID, + RepositoryID: repositoryID, + MergeRequestID: mergeRequestID, + ReturnURL: originURL, + } + + log.WithFields(f).Debugf("active signature metadata: %+v", storeValue) + + json_data, err := json.Marshal(storeValue) + if err != nil { + msg := fmt.Sprintf("unable to marshall storeValue object: %+v", storeValue) + log.WithFields(f).Warn(msg) + return nil, err + } + expire := time.Now().AddDate(0, 0, 1).Unix() + log.WithFields(f).Debugf("setting expiry for active signature data to : %d", expire) + log.WithFields(f).Debugf("json data: %s", string(json_data)) + + activeSigErr := s.storeRepo.SetActiveSignatureMetaData(ctx, key, expire, string(json_data)) + if activeSigErr != nil { + log.WithFields(f).WithError(err).Warn("unable to save signature metadata") + return nil, activeSigErr + } + + params := "redirect=" + url.QueryEscape(originURL) + consoleURL := fmt.Sprintf("https://%s/#/cla/project/%s/user/%s?%s", contributorBaseURL, gitlabRepo.RepositoryClaGroupID, claUser.UserID, params) + _, err = http.Get(consoleURL) + + if err != nil { + msg := fmt.Sprintf("unable to redirect to : %s , error: %+v ", consoleURL, err) + log.WithFields(f).Warn(msg) + return nil, err + } + var signatureID string + icla, signErr := s.signatureRepo.GetIndividualSignature(ctx, gitlabRepo.RepositoryClaGroupID, claUser.UserID, aws.Bool(false), aws.Bool(true)) + if signErr != nil { + log.WithFields(f).WithError(signErr).Warnf("problem checking for ICLA signature for user: %s", claUser.UserID) + } + if icla != nil && icla.SignatureID != "" { + log.WithFields(f).Infof("loaded individual signature id: %s for claGroupID: %s and UserID: %s", icla.SignatureID, gitlabRepo.RepositoryClaGroupID, claUser.UserID) + signatureID = icla.SignatureID + } else { + log.WithFields(f).Debugf("ICLA signature check failed for user: %+v on project: %s - ICLA not signed", claUser, gitlabRepo.RepositoryClaGroupID) + if claUser.CompanyID == "" { + log.WithFields(f).Debugf("user does not have association with any company, can't confirm employee acknoledgement") + return &consoleURL, nil + } + + companyID := claUser.CompanyID + _, err = s.companyRepository.GetCompany(ctx, companyID) + if err != nil { + msg := fmt.Sprintf("can't load company record: %s for user: %s (%s), error: %v", companyID, claUser.Username, claUser.UserID, err) + log.WithFields(f).Errorf(msg) + return &consoleURL, nil + } + + corporateSignature, err := s.signatureRepo.GetCorporateSignature(ctx, gitlabRepo.RepositoryClaGroupID, companyID, aws.Bool(false), aws.Bool(true)) + if err != nil { + msg := fmt.Sprintf("can't load company signature record for company: %s for user : %s (%s), error : %v", companyID, claUser.Username, claUser.UserID, err) + log.WithFields(f).Errorf(msg) + return &consoleURL, nil + } + + if corporateSignature == nil { + msg := fmt.Sprintf("no corporate signature (CCLA) record found for company : %s ", companyID) + log.WithFields(f).Errorf(msg) + return &consoleURL, nil + } + log.WithFields(f).Debugf("loaded corporate signature id: %s for claGroupID: %s and companyID: %s", corporateSignature.SignatureID, gitlabRepo.RepositoryClaGroupID, companyID) + signatureID = corporateSignature.SignatureID + } + err = s.signatureRepo.ActivateSignature(ctx, signatureID) + if err != nil { + msg := fmt.Sprintf("found error on Activate Signature error : %s", err.Error()) + log.WithFields(f).Errorf(msg) + } + return &consoleURL, nil +} + +func (s *Service) getOrCreateUser(ctx context.Context, gitlabClient *goGitLab.Client, eventsService events.Service) (*models.User, error) { + + f := logrus.Fields{ + "functionName": "v2.gitlab_sign.service.getOrCreateUser", + } + + gitlabUser, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + log.WithFields(f).Debugf("getting gitlab current user for failed : %v ", err) + return nil, err + } + + claUser, err := s.userService.GetUserByGitlabID(gitlabUser.ID) + if err != nil { + log.WithFields(f).Debugf("unable to get CLA user by github ID: %d , error: %+v ", gitlabUser.ID, err) + log.WithFields(f).Infof("creating user record for gitlab user : %+v ", gitlabUser) + user := &models.User{ + GitlabID: fmt.Sprintf("%d", gitlabUser.ID), + GitlabUsername: gitlabUser.Username, + Emails: []string{gitlabUser.Email}, + Username: gitlabUser.Name, + } + + var userErr error + claUser, userErr = s.userService.CreateUser(user, nil) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to create user with details : %+v", user) + return nil, userErr + } + + // Log the event + eventsService.LogEvent(&events.LogEventArgs{ + EventType: events.UserCreated, + UserID: user.UserID, + UserModel: user, + EventData: &events.UserCreatedEventData{}, + }) + return claUser, nil + } + return claUser, nil +} + +func buildInstallationURL(gitlabOrgID string, authStateNonce string) *strfmt.URI { + base := "https://gitlab.com/oauth/authorize" + c := config.GetConfig() + state := fmt.Sprintf("%s:%s", gitlabOrgID, authStateNonce) + + params := url.Values{} + params.Add("client_id", c.Gitlab.AppClientID) + params.Add("redirect_uri", c.Gitlab.RedirectURI) + //params.Add("redirect_uri", "http://localhost:8080/v4/gitlab/oauth/callback") + params.Add("response_type", "code") + params.Add("state", state) + params.Add("scope", "api read_user read_api read_repository write_repository email") + + installationURL := strfmt.URI(base + "?" + params.Encode()) + return &installationURL +} + +func toGitLabProjectResponse(gitLabListRepos *v2Models.GitlabRepositoriesList) []*v2Models.GitlabProjectRepository { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.toGitLabProjectResponse", + } + + if gitLabListRepos == nil { + return []*v2Models.GitlabProjectRepository{} + } + + var repoList []*v2Models.GitlabProjectRepository + for _, repo := range gitLabListRepos.List { + parentProjectSFID, err := projectService.GetClient().GetParentProject(repo.RepositoryProjectSfid) + if err != nil { + log.WithFields(f).Warnf("unable to lookup project parent SFID using SFID: %s", repo.RepositoryProjectSfid) + } + + repoList = append(repoList, &v2Models.GitlabProjectRepository{ + ClaGroupID: repo.RepositoryClaGroupID, + //ConnectionStatus: "", // set via another function + Enabled: repo.Enabled, + ParentProjectID: parentProjectSFID, + ProjectID: repo.RepositoryProjectSfid, + RepositoryGitlabID: repo.RepositoryExternalID, + RepositoryID: repo.RepositoryID, + RepositoryName: repo.RepositoryName, + RepositoryFullPath: repo.RepositoryFullPath, + RepositoryURL: repo.RepositoryURL, + }) + } + + return repoList +} + +// updateRepositoryStatus is a helper function to set/add the repository connection status +func (s *Service) updateRepositoryStatus(glClient *goGitLab.Client, gitLabProjectRepos []*v2Models.GitlabProjectRepository) []*v2Models.GitlabProjectRepository { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.updateRepositoryStatus", + } + + if gitLabProjectRepos == nil { + return []*v2Models.GitlabProjectRepository{} + } + + type responseChannelModel struct { + RepositoryExternalID int64 + ConnectionStatus string + Error error + } + // A channel for the responses from the go routines + responseChannel := make(chan responseChannelModel) + + opts := &goGitLab.GetProjectOptions{} + for _, repo := range gitLabProjectRepos { + // Create a go routine to this concurrently + go func(glClient *goGitLab.Client, repo *v2Models.GitlabProjectRepository) { + projectModel, resp, lookupErr := glClient.Projects.GetProject(int(repo.RepositoryGitlabID), opts) // OK to convert int64 to int as it is the ID and probably should be typed as a int anyway + if lookupErr != nil { + log.WithFields(f).WithError(lookupErr).Warnf("problem loading GitLab project by external ID: %d, error: %v", repo.RepositoryGitlabID, lookupErr) + repo.ConnectionStatus = utils.ConnectionFailure + responseChannel <- responseChannelModel{ + RepositoryExternalID: repo.RepositoryGitlabID, + ConnectionStatus: utils.ConnectionFailure, + Error: lookupErr, + } + return + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + responseBody, readErr := io.ReadAll(resp.Body) + if readErr != nil { + log.WithFields(f).WithError(lookupErr).Warnf("problem loading GitLab project by external ID: %d, error: %v", repo.RepositoryGitlabID, readErr) + responseChannel <- responseChannelModel{ + RepositoryExternalID: repo.RepositoryGitlabID, + ConnectionStatus: utils.ConnectionFailure, + Error: readErr, + } + return + } + msg := fmt.Sprintf("problem loading GitLab project by external ID: %d, response code: %d, body: %s", repo.RepositoryGitlabID, resp.StatusCode, responseBody) + log.WithFields(f).Warnf(msg) + responseChannel <- responseChannelModel{ + RepositoryExternalID: repo.RepositoryGitlabID, + ConnectionStatus: utils.ConnectionFailure, + Error: errors.New(msg), + } + return + } + + log.WithFields(f).Debugf("connected to project/repo: %s", projectModel.PathWithNamespace) + responseChannel <- responseChannelModel{ + RepositoryExternalID: repo.RepositoryGitlabID, + ConnectionStatus: utils.Connected, + Error: nil, + } + }(glClient, repo) + } + + for i := 0; i < len(gitLabProjectRepos); i++ { + resp := <-responseChannel + // Find the matching repo for this response + for _, repo := range gitLabProjectRepos { + if repo.RepositoryGitlabID == resp.RepositoryExternalID { + repo.ConnectionStatus = resp.ConnectionStatus + } + } + if resp.Error != nil { + log.WithFields(f).Warnf("problem connecting to GitLab repoistory, error: %+v", resp.Error) + } + } + + return gitLabProjectRepos +} diff --git a/cla-backend-go/v2/gitlab_sign/handlers.go b/cla-backend-go/v2/gitlab_sign/handlers.go new file mode 100644 index 000000000..591dfa2c7 --- /dev/null +++ b/cla-backend-go/v2/gitlab_sign/handlers.go @@ -0,0 +1,121 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_sign + +import ( + "context" + "fmt" + "net/http" + + "github.com/communitybridge/easycla/cla-backend-go/config" + "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_sign" + gitlabApi "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + "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" + "golang.org/x/oauth2" + oauth_gitlab "golang.org/x/oauth2/gitlab" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" +) + +const ( + // SessionStoreKey for cla-gitlab session + SessionStoreKey = "cla-gitlab" +) + +func Configure(api *operations.EasyclaAPI, service Service, eventService events.Service, contributorConsoleV2Base string, sessionStore *dynastore.Store) { + api.GitlabSignSignRequestHandler = gitlab_sign.SignRequestHandlerFunc( + func(srp gitlab_sign.SignRequestParams) middleware.Responder { + reqID := utils.GetRequestID(srp.XREQUESTID) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) + + f := logrus.Fields{ + "functionName": "v2.gitlab_sign.handlers.GitlabSignSignRequestHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "installationID": srp.OrganizationID, + "repositoryID": srp.GitlabRepositoryID, + "mergeRequestID": srp.MergeRequestID, + } + + return middleware.ResponderFunc(func(rw http.ResponseWriter, pr runtime.Producer) { + session, err := sessionStore.Get(srp.HTTPRequest, SessionStoreKey) + if err != nil { + log.WithFields(f).WithError(err).Warn("error with session store lookup") + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + config := config.GetConfig() + + log.WithFields(f).Debugf("Loading session : %+v", session) + + log.WithFields(f).Debug("Initiating sign request..") + + session.Values["gitlab_installation_id"] = srp.OrganizationID + session.Values["gitlab_repository_id"] = srp.GitlabRepositoryID + session.Values["gitlab_merge_request_id"] = srp.MergeRequestID + + originURL, err := service.GetOriginURL(ctx, srp.OrganizationID, srp.GitlabRepositoryID, srp.MergeRequestID) + if err != nil { + log.WithFields(f).WithError(err).Warn("error getting origin URL") + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + session.Values["gitlab_origin_url"] = *originURL + + gitlabAuthToken, ok := session.Values["gitlab_oauth2_token"].(string) + if ok { + session.Save(srp.HTTPRequest, rw) + log.WithFields(f).Debugf("using existing Gitlab Ouath2 Token: %s ", gitlabAuthToken) + gitlabClient, err := gitlabApi.NewGitlabOauthClientFromAccessToken(gitlabAuthToken) + + if err != nil { + msg := fmt.Sprintf("problem creating gitlab client with token : %s ", gitlabAuthToken) + log.WithFields(f).Debug(msg) + http.Error(rw, msg, http.StatusInternalServerError) + } + + log.WithFields(f).Debugf("Initiating Gitlab sign request for : %+v ", srp) + + consoleURL, err := service.InitiateSignRequest(ctx, srp.HTTPRequest, gitlabClient, srp.GitlabRepositoryID, srp.MergeRequestID, *originURL, contributorConsoleV2Base, eventService) + + if err != nil { + msg := fmt.Sprintf("problem initiating sign request for :%+v", srp) + log.WithFields(f).Debugf(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + + http.Redirect(rw, srp.HTTPRequest, *consoleURL, http.StatusSeeOther) + } + + log.WithFields(f).Debugf("No existing GitLab Oauth2 Token ") + + log.WithFields(f).Debug("initiating gitlab sign request ...") + stateID, err := uuid.NewV4() + state := fmt.Sprintf("user:%s", stateID.String()) + session.Values["gitlab_oauth2_state"] = state + session.Save(srp.HTTPRequest, rw) + oauthConfig := &oauth2.Config{ + ClientID: config.Gitlab.AppClientID, + Scopes: []string{ + "read_user", + "email", + }, + Endpoint: oauth_gitlab.Endpoint, + RedirectURL: config.Gitlab.RedirectURI, + } + session.Values["gitlab_oauth2_state"] = state + session.Save(srp.HTTPRequest, rw) + http.Redirect(rw, srp.HTTPRequest, oauthConfig.AuthCodeURL(state), http.StatusFound) + }) + + }) +} diff --git a/cla-backend-go/v2/gitlab_sign/service.go b/cla-backend-go/v2/gitlab_sign/service.go new file mode 100644 index 000000000..dcb643854 --- /dev/null +++ b/cla-backend-go/v2/gitlab_sign/service.go @@ -0,0 +1,242 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_sign + +import ( + "context" + "encoding/json" + + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/go-openapi/strfmt" + + // "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + + "time" + + "github.com/sirupsen/logrus" + + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + 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/users" + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" + "github.com/communitybridge/easycla/cla-backend-go/v2/store" + "github.com/xanzy/go-gitlab" +) + +type service struct { + repoService repositories.ServiceInterface + gitlabOrgService gitlab_organizations.ServiceInterface + userService users.Service + gitlabApp *gitlab_api.App + storeRepo store.Repository +} + +type Service interface { + InitiateSignRequest(ctx context.Context, req *http.Request, gitlabClient *gitlab.Client, repositoryID, mergeRequestID, originURL, contributorBaseURL string, eventService events.Service) (*string, error) + GetOriginURL(ctx context.Context, organizationID, repositoryID, mergeRequestID string) (*string, error) +} + +func NewService(gitlabRepositoryService repositories.ServiceInterface, userService users.Service, storeRepo store.Repository, gitlabApp *gitlab_api.App, gitlabOrgService gitlab_organizations.ServiceInterface) Service { + return &service{ + repoService: gitlabRepositoryService, + userService: userService, + gitlabApp: gitlabApp, + storeRepo: storeRepo, + gitlabOrgService: gitlabOrgService, + } +} + +// GetOriginURL Gets Origin URL for the newly created MR +func (s service) GetOriginURL(ctx context.Context, organizationID, repositoryID, mergeRequestID string) (*string, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_sign.service.GetOriginURL", + "organizationID": organizationID, + } + organization, err := s.gitlabOrgService.GetGitLabOrganization(ctx, organizationID) + if err != nil { + log.WithFields(f).Debugf("unable to get gitlab organiztion by ID: %s, error: %+v ", organizationID, err) + return nil, err + } + + if organization.AuthInfo == "" { + msg := fmt.Sprintf("organization: %s has no auth details", organizationID) + log.WithFields(f).Debug(msg) + return nil, errors.New(msg) + } + + oauthResponse, err := s.gitlabOrgService.RefreshGitLabOrganizationAuth(ctx, common.ToCommonModel(organization)) + if err != nil { + log.WithFields(f).Debugf("unable to refresh gitlab organiztion auth by ID: %s, error: %+v ", organizationID, err) + return nil, err + } + + gitlabClient, err := gitlab_api.NewGitlabOauthClient(*oauthResponse, s.gitlabApp) + if err != nil { + log.WithFields(f).Debugf("initializaing gitlab client for gitlab org: %s failed: %v", organizationID, err) + return nil, err + } + + mergeRequestIDInt, err := strconv.Atoi(mergeRequestID) + if err != nil { + log.WithFields(f).Debugf("unable to convert organization string value : %s to Int", organizationID) + return nil, err + } + + log.WithFields(f).Debug("Determining return URL from the inbound request ...") + mergeRequest, _, err := gitlabClient.MergeRequests.GetMergeRequest(repositoryID, mergeRequestIDInt, &gitlab.GetMergeRequestsOptions{}) + if err != nil || mergeRequest == nil { + log.WithFields(f).Debugf("unable to fetch MR Web URL: mergeRequestID: %s ", mergeRequestID) + return nil, err + } + + originURL := mergeRequest.WebURL + log.WithFields(f).Debugf("Return URL from the inbound request is : %s ", originURL) + + return &originURL, nil +} + +// InitiateSignRequest initiates sign request and returns easy cla redirect url +func (s service) InitiateSignRequest(ctx context.Context, req *http.Request, gitlabClient *gitlab.Client, repositoryID, mergeRequestID, originURL, contributorBaseURL string, eventService events.Service) (*string, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_sign.service.redirectToConsole", + "repositoryID": repositoryID, + "mergeRequestID": mergeRequestID, + "originURL": originURL, + } + + claUser, err := s.getOrCreateUser(ctx, gitlabClient, eventService) + if err != nil { + msg := fmt.Sprintf("unable to get or create user : %+v ", err) + log.WithFields(f).Warn(msg) + return nil, err + } + + repoIDInt, err := strconv.Atoi(repositoryID) + if err != nil { + msg := fmt.Sprintf("unable to convert GitlabRepoID: %s to int", repositoryID) + log.WithFields(f).Warn(msg) + return nil, err + } + + log.WithFields(f).Debugf("getting gitlab repository for: %d", repoIDInt) + gitlabRepo, err := s.repoService.GitLabGetRepositoryByExternalID(ctx, int64(repoIDInt)) + if err != nil { + msg := fmt.Sprintf("unable to find repository by ID: %s , error: %+v ", repositoryID, err) + log.WithFields(f).Warn(msg) + return nil, err + } + + type StoreValue struct { + UserID string `json:"user_id"` + ProjectID string `json:"project_id"` + RepositoryID string `json:"repository_id"` + MergeRequestID string `json:"merge_request_id"` + ReturnURL string `json:"return_url"` + } + + log.WithFields(f).Debugf("setting active signature metadata: claUser: %+v, repository: %+v", claUser, gitlabRepo) + // set active signature metadata to track the user signing process + key := fmt.Sprintf("active_signature:%s", claUser.UserID) + storeValue := StoreValue{ + UserID: claUser.UserID, + ProjectID: gitlabRepo.RepositoryClaGroupID, + RepositoryID: repositoryID, + MergeRequestID: mergeRequestID, + ReturnURL: originURL, + } + json_data, err := json.Marshal(storeValue) + if err != nil { + log.Fatal(err) + } + expire := time.Now().AddDate(0, 0, 1).Unix() + + // jsonVal, _ := json.Marshal(value) + + activeSignErr := s.storeRepo.SetActiveSignatureMetaData(ctx, key, expire, string(json_data)) + if activeSignErr != nil { + log.WithFields(f).WithError(activeSignErr).Warn("unable to save signature metadata") + return nil, activeSignErr + } + + params := "redirect=" + url.QueryEscape(originURL) + consoleURL := fmt.Sprintf("https://%s/#/cla/project/%s/user/%s?%s", contributorBaseURL, gitlabRepo.RepositoryClaGroupID, claUser.UserID, params) + _, err = http.Get(consoleURL) + + if err != nil { + msg := fmt.Sprintf("unable to redirect to : %s , error: %+v ", consoleURL, err) + log.WithFields(f).Warn(msg) + return nil, err + } + + return &consoleURL, nil +} + +func (s service) getOrCreateUser(ctx context.Context, gitlabClient *gitlab.Client, eventsService events.Service) (*models.User, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_sign.service.getOrCreateUser", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + gitlabUser, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + log.WithFields(f).Debugf("getting gitlab current user for failed : %v ", err) + return nil, err + } + + log.WithFields(f).Debugf("looking up user by GitLab ID: %d", gitlabUser.ID) + claUser, err := s.userService.GetUserByGitlabID(gitlabUser.ID) + if err == nil && claUser != nil { + log.WithFields(f).Debugf("found user by GitLab ID: %d", gitlabUser.ID) + return claUser, nil + } + log.WithFields(f).Debugf("unable to lookup user by github ID: %d, error: %+v ", gitlabUser.ID, err) + + log.WithFields(f).Debugf("looking up user by GitLab username: %s", gitlabUser.Username) + claUser, err = s.userService.GetUserByGitLabUsername(gitlabUser.Username) + if err == nil && claUser != nil { + log.WithFields(f).Debugf("found user by GitLab username: %s", gitlabUser.Username) + return claUser, nil + } + log.WithFields(f).Debugf("unable to lookup user by github username: %s, error: %+v ", gitlabUser.Username, err) + + log.WithFields(f).Debugf("looking up user by GitLab email: %s", gitlabUser.Email) + claUser, err = s.userService.GetUserByEmail(gitlabUser.Email) + if err == nil && claUser != nil { + log.WithFields(f).Debugf("found user by GitLab email: %s", gitlabUser.Email) + return claUser, nil + } + + log.WithFields(f).Infof("unable to locate GitLab user - creating a new user record for GitLab user : %+v ", gitlabUser) + user := &models.User{ + GitlabID: fmt.Sprintf("%d", gitlabUser.ID), + GitlabUsername: gitlabUser.Username, + LfEmail: strfmt.Email(gitlabUser.Email), + Emails: []string{gitlabUser.Email}, + Username: gitlabUser.Name, + } + claUser, userErr := s.userService.CreateUser(user, nil) + if err != nil { + log.WithFields(f).Debugf("unable to create claUser with details : %+v, error: %+v", user, userErr) + return nil, userErr + } + + // Log the event + eventsService.LogEvent(&events.LogEventArgs{ + EventType: events.UserCreated, + UserID: user.UserID, + UserModel: user, + EventData: &events.UserCreatedEventData{}, + }) + + return claUser, nil +} diff --git a/cla-backend-go/v2/metrics/handlers.go b/cla-backend-go/v2/metrics/handlers.go index 05edfaaa1..b17552ca1 100644 --- a/cla-backend-go/v2/metrics/handlers.go +++ b/cla-backend-go/v2/metrics/handlers.go @@ -7,11 +7,14 @@ import ( "context" "fmt" + "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/metrics" + log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/go-openapi/runtime/middleware" ) @@ -95,22 +98,29 @@ func Configure(api *operations.EasyclaAPI, service Service, v1CompanyRepo v1Comp func(params metrics.ListCompanyProjectMetricsParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + f := logrus.Fields{ + "functionName": "MetricsListCompanyProjectMetricsHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": params.CompanyID, + } + // Lookup the company by internal ID + log.WithFields(f).Debugf("looking up company by internal ID...") + company, compErr := v1CompanyRepo.GetCompany(ctx, params.CompanyID) + if compErr != nil { + log.WithFields(f).Warnf("unable to fetch company by ID:%s ", params.CompanyID) + return metrics.NewListCompanyProjectMetricsBadRequest().WithPayload(errorResponse(reqID, compErr)) + } utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - if !utils.IsUserAuthorizedForOrganization(authUser, params.CompanySFID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForOrganization(ctx, authUser, company.CompanyExternalID, utils.ALLOW_ADMIN_SCOPE) { return metrics.NewListCompanyProjectMetricsForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "403", Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to List Company Project Metrics with Organization scope of %s", - authUser.UserName, params.CompanySFID), + authUser.UserName, company.CompanyExternalID), XRequestID: reqID, }) } - comp, err := v1CompanyRepo.GetCompanyByExternalID(ctx, params.CompanySFID) - if err != nil { - if err == v1Company.ErrCompanyDoesNotExist { - return metrics.NewListCompanyProjectMetricsNotFound().WithXRequestID(reqID) - } - } - result, err := service.ListCompanyProjectMetrics(comp.CompanyID, params.ProjectSFID) + + result, err := service.ListCompanyProjectMetrics(ctx, params.CompanyID, params.ProjectSFID) if err != nil { return metrics.NewListCompanyProjectMetricsBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } diff --git a/cla-backend-go/v2/metrics/repository.go b/cla-backend-go/v2/metrics/repository.go index 7876ec48e..ea88f7bd9 100644 --- a/cla-backend-go/v2/metrics/repository.go +++ b/cla-backend-go/v2/metrics/repository.go @@ -4,6 +4,7 @@ package metrics import ( + "context" "errors" "fmt" "time" @@ -87,10 +88,10 @@ const ( MetricTypeCompany = "company" MetricTypeProject = "project" MetricTypeCompanyProject = "company_project" - MetricTypeClaManagerDistribution = "cla_manager_distribution" + MetricTypeClaManagerDistribution = "cla_manager_distribution" // nolint G101: Potential hardcoded credentials (gosec) IDTotalCount = "total_count" - IDClaManagerDistribution = "cla_manager_distribution" + IDClaManagerDistribution = "cla_manager_distribution" // nolint G101: Potential hardcoded credentials (gosec) ) func newMetrics() *Metrics { @@ -756,7 +757,7 @@ func (repo *repo) calculateMetrics() (*Metrics, error) { return nil, err } - log.Debug("Calculating Repository metrics...") + log.Debug("Calculating CombinedRepository metrics...") // calculate github repositories count // increment project repositories count err = repo.processRepositoriesTable(metrics) @@ -967,7 +968,7 @@ type claGroup struct { func (repo *repo) getClaGroupProjectsMapping() (map[string]*claGroup, error) { r := make(map[string]*claGroup) - cgpList, err := repo.projectsClaGroupsRepo.GetProjectsIdsForAllFoundation() + cgpList, err := repo.projectsClaGroupsRepo.GetProjectsIdsForAllFoundation(context.Background()) if err != nil { return r, err } @@ -1073,7 +1074,7 @@ func (repo *repo) projectHelperMap(in *CompanyProjectMetrics, claGroupMapping ma log.Printf("length of projectIDArray %d", len(projectIDArray)) for projectSFID := range projectIDArray { - projectData, err := repo.projectsClaGroupsRepo.GetClaGroupIDForProject(projectSFID) + projectData, err := repo.projectsClaGroupsRepo.GetClaGroupIDForProject(context.Background(), projectSFID) if err != nil { log.Warnf("projectHelperMap/GetClaGroupIDForProject error = unable to get project details from easycla. %s", projectSFID) continue diff --git a/cla-backend-go/v2/metrics/service.go b/cla-backend-go/v2/metrics/service.go index 13da21b41..0b9bd6194 100644 --- a/cla-backend-go/v2/metrics/service.go +++ b/cla-backend-go/v2/metrics/service.go @@ -4,6 +4,7 @@ package metrics import ( + "context" "errors" "math" "sort" @@ -35,7 +36,7 @@ type Service interface { GetTopCompanies() (*models.TopCompanies, error) GetTopProjects() (*models.TopProjects, error) ListProjectMetrics(paramPageSize *int64, paramNextKey *string) (*models.ListProjectMetric, error) - ListCompanyProjectMetrics(companyID string, projectSFID string) (*models.CompanyProjectMetrics, error) + ListCompanyProjectMetrics(ctx context.Context, companyID string, projectSFID string) (*models.CompanyProjectMetrics, error) } type service struct { @@ -287,7 +288,7 @@ func (s *service) ListProjectMetrics(paramPageSize *int64, paramNextKey *string) return &out, nil } -func (s *service) ListCompanyProjectMetrics(companyID string, projectSFID string) (*models.CompanyProjectMetrics, error) { +func (s *service) ListCompanyProjectMetrics(ctx context.Context, companyID string, projectSFID string) (*models.CompanyProjectMetrics, error) { psc := project_service.GetClient() claGroupList := utils.NewStringSet() project, err := psc.GetProject(projectSFID) @@ -295,7 +296,7 @@ func (s *service) ListCompanyProjectMetrics(companyID string, projectSFID string return nil, err } if project.ProjectType == FoundationType { - cgmList, cgerr := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(projectSFID) + cgmList, cgerr := s.projectsClaGroupsRepo.GetProjectsIdsForFoundation(ctx, projectSFID) if cgerr != nil { return nil, err } @@ -303,7 +304,7 @@ func (s *service) ListCompanyProjectMetrics(companyID string, projectSFID string claGroupList.Add(cgm.ClaGroupID) } } else { - cgm, cgerr := s.projectsClaGroupsRepo.GetClaGroupIDForProject(projectSFID) + cgm, cgerr := s.projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, projectSFID) if cgerr != nil { return nil, err } diff --git a/cla-backend-go/v2/organization-service/client.go b/cla-backend-go/v2/organization-service/client.go index 39867f6f8..4b6c4ee83 100644 --- a/cla-backend-go/v2/organization-service/client.go +++ b/cla-backend-go/v2/organization-service/client.go @@ -29,7 +29,7 @@ import ( // Client is client for organization_service type Client struct { - cl *client.OrganziationService + cl *client.OrganizationServiceAPI } const ( @@ -71,7 +71,7 @@ func (osc *Client) CreateOrgUserRoleOrgScope(ctx context.Context, emailID string params := &organizations.CreateOrgUsrRoleScopesParams{ CreateRoleScopes: &models.CreateRolescopes{ - EmailAddress: &emailID, + EmailAddress: emailID, ObjectID: &organizationID, ObjectType: aws.String("organization"), RoleID: &roleID, @@ -91,7 +91,11 @@ func (osc *Client) CreateOrgUserRoleOrgScope(ctx context.Context, emailID string result, err := osc.cl.Organizations.CreateOrgUsrRoleScopes(params, clientAuth) if err != nil { log.WithFields(f).WithError(err).Warn("unable to assign user to organization") - return err + _, ok := err.(*organizations.CreateOrgUsrRoleScopesConflict) + if !ok { + return err + } + log.WithFields(f).Warn("the role already assigned for the user skipping") } log.WithFields(f).Debugf("Successfully assigned user to organization, result: %#v", result) @@ -215,7 +219,7 @@ func (osc *Client) CreateOrgUserRoleOrgScopeProjectOrg(ctx context.Context, emai params := &organizations.CreateOrgUsrRoleScopesParams{ CreateRoleScopes: &models.CreateRolescopes{ - EmailAddress: &emailID, + EmailAddress: emailID, ObjectID: aws.String(fmt.Sprintf("%s|%s", projectID, organizationID)), ObjectType: aws.String("project|organization"), RoleID: &roleID, @@ -283,15 +287,12 @@ func (osc *Client) DeleteRolePermissions(ctx context.Context, organizationID, pr // Log Event... v1EventService.LogEvent(&events.LogEventArgs{ - EventType: events.ClaManagerRoleDeleted, - ProjectID: projectID, - ClaGroupModel: nil, - CompanyID: organizationID, - CompanyModel: nil, - LfUsername: authUser.UserName, - UserID: authUser.UserName, - UserModel: nil, - ExternalProjectID: projectID, + EventType: events.ClaManagerRoleDeleted, + ProjectID: projectID, + ProjectSFID: projectID, + CompanyID: organizationID, + LfUsername: authUser.UserName, + UserID: authUser.UserName, EventData: &events.ClaManagerRoleDeletedData{ Role: role, // cla-manager Scope: scope.ObjectTypeName, // project|organization @@ -532,6 +533,8 @@ func (osc *Client) CreateOrg(ctx context.Context, companyName, signingEntityName "companyWebsite": companyWebsite, } + var org *models.Organization + tok, tokenErr := token.GetToken() if tokenErr != nil { log.WithFields(f).WithError(tokenErr).Warn("unable to fetch token") @@ -543,74 +546,67 @@ func (osc *Client) CreateOrg(ctx context.Context, companyName, signingEntityName signingEntityName = companyName } - // Search for an existing record by website - existingRecords, lookupErr := osc.SearchOrganization(ctx, "", companyWebsite, "") + //Lookup Org based on domain + lookupOrg, lookupErr := osc.SearchOrgLookup(ctx, nil, &companyWebsite) if lookupErr != nil { log.WithFields(f).WithError(lookupErr).Warn("unable to search for existing company using company website value") - return nil, lookupErr + if _, ok := lookupErr.(*organizations.LookupNotFound); !ok { + return nil, lookupErr + } } - // If we have an existing record... should only be one record if any - if len(existingRecords) > 0 { - updatedModel, updateErr := osc.UpdateOrg(ctx, existingRecords[0], signingEntityName) + if lookupOrg != nil && lookupOrg.Payload.ID != "" { + // Get org based on ID + var updateErr error + existingOrg, existingOrgErr := osc.GetOrganization(ctx, lookupOrg.Payload.ID) + if existingOrgErr != nil { + log.WithFields(f).WithError(existingOrgErr).Warnf("unable to get organization : %s ", lookupOrg.Payload.ID) + return nil, existingOrgErr + } + org, updateErr = osc.UpdateOrg(ctx, existingOrg, signingEntityName) if updateErr != nil { log.WithFields(f).WithError(updateErr).Warn("unable to update for existing company") return nil, updateErr } - return updatedModel, nil - } - // use linux foundation logo as default - linuxFoundation, err := osc.SearchOrganization(ctx, utils.TheLinuxFoundation, "", "") - if err != nil || len(linuxFoundation) == 0 { - log.WithFields(f).WithError(err).Warn("unable to search Linux Foundation organization") - return nil, err - } + } else { + // use linux foundation logo as default + linuxFoundation, err := osc.SearchOrganization(ctx, utils.TheLinuxFoundation, "", "") + if err != nil || len(linuxFoundation) == 0 { + log.WithFields(f).WithError(err).Warn("unable to search Linux Foundation organization") + return nil, err + } - clientAuth := runtimeClient.BearerToken(tok) - description := "No Description" - f["description"] = description - companyType := "Customer" - f["type"] = companyType - f["companyType"] = companyType - companySource := "No Source" - industry := "No Industry" - f["industry"] = industry - logoURL := linuxFoundation[0].LogoURL - f["logoURL"] = logoURL - - params := &organizations.CreateOrgParams{ - Org: &models.CreateOrg{ - Description: &description, + clientAuth := runtimeClient.BearerToken(tok) + logoURL := linuxFoundation[0].LogoURL + f["logoURL"] = logoURL + + params := &organizations.CreateOrgParams{ + Org: &models.CreateOrg{ + Name: &companyName, + Website: &companyWebsite, + LogoURL: logoURL, + SigningEntityName: []string{signingEntityName}, + }, + Context: ctx, + } + + log.WithFields(f).Debugf("Creating organization with params: %+v", models.CreateOrg{ Name: &companyName, Website: &companyWebsite, - Industry: &industry, - Source: &companySource, - Type: &companyType, - LogoURL: &logoURL, + LogoURL: logoURL, SigningEntityName: []string{signingEntityName}, - }, - Context: ctx, - } + }) + result, err := osc.cl.Organizations.CreateOrg(params, clientAuth) + if err != nil { + log.WithFields(f).WithError(err).Warnf("Failed to create salesforce Company: %s , err: %+v ", companyName, err) + return nil, err + } + log.WithFields(f).Infof("Company: %s successfuly created ", companyName) - log.WithFields(f).Debugf("Creating organization with params: %+v", models.CreateOrg{ - Description: &description, - Name: &companyName, - Website: &companyWebsite, - Industry: &industry, - Source: &companySource, - Type: &companyType, - LogoURL: &logoURL, - SigningEntityName: []string{signingEntityName}, - }) - result, err := osc.cl.Organizations.CreateOrg(params, clientAuth) - if err != nil { - log.WithFields(f).WithError(err).Warnf("Failed to create salesforce Company :%s , err: %+v ", companyName, err) - return nil, err + org = result.Payload } - log.WithFields(f).Infof("Company: %s successfuly created ", companyName) - - return result.Payload, err + return org, nil } // UpdateOrg updates the company record based on the provided name, signingEntityName, and website @@ -685,12 +681,13 @@ func (osc *Client) ListOrg(ctx context.Context, orgName string) (*models.Organiz } // SearchOrgLookup returns organization -func (osc *Client) SearchOrgLookup(orgName string, websiteName string) (*organizations.LookupOK, error) { +func (osc *Client) SearchOrgLookup(ctx context.Context, orgName, websiteName *string) (*organizations.LookupOK, error) { f := logrus.Fields{ "functionName": "organization_service.Lookup", - "orgName": orgName, - "websiteName": websiteName, + "orgName": utils.StringValue(orgName), + "websiteName": utils.StringValue(websiteName), } + tok, err := token.GetToken() if err != nil { log.WithFields(f).WithError(err).Warn("unable to fetch token") @@ -699,9 +696,13 @@ func (osc *Client) SearchOrgLookup(orgName string, websiteName string) (*organiz clientAuth := runtimeClient.BearerToken(tok) params := &organizations.LookupParams{ - Name: aws.String(orgName), - Domain: aws.String(websiteName), - Context: context.TODO(), + Context: ctx, + } + if orgName != nil { + params.Name = orgName + } + if websiteName != nil { + params.Domain = websiteName } result, err := osc.cl.Organizations.Lookup(params, clientAuth) if err != nil { @@ -710,4 +711,5 @@ func (osc *Client) SearchOrgLookup(orgName string, websiteName string) (*organiz } return result, nil + } diff --git a/cla-backend-go/v2/project-service/client.go b/cla-backend-go/v2/project-service/client.go index cae1349fc..4eaad243e 100644 --- a/cla-backend-go/v2/project-service/client.go +++ b/cla-backend-go/v2/project-service/client.go @@ -4,7 +4,11 @@ package project_service import ( + "context" + "errors" + "fmt" "strings" + "sync" "github.com/sirupsen/logrus" @@ -29,20 +33,25 @@ const ( // Client is client for user_service type Client struct { - cl *client.PMM + cl *client.PMMAPI } var ( projectServiceClient *Client + // mutex is an object to allow us to lock access to the shared project service map while used by multiple go routines + mutex = &sync.Mutex{} + // Short term cache - only for the lifetime of this lambda + projectServiceModels = make(map[string]*models.ProjectOutputDetailed) + apiGWHost string ) // InitClient initializes the user_service client func InitClient(APIGwURL string) { - APIGwURL = strings.ReplaceAll(APIGwURL, "https://", "") + apiGWHost = strings.ReplaceAll(APIGwURL, "https://", "") projectServiceClient = &Client{ cl: client.NewHTTPClientWithConfig(strfmt.Default, &client.TransportConfig{ - Host: APIGwURL, - BasePath: "project-service/v1", + Host: apiGWHost, + BasePath: "project-service", Schemes: []string{"https"}, }), } @@ -65,61 +74,228 @@ func (pmm *Client) getProject(projectSFID string, auth runtime.ClientAuthInfoWri // GetProject returns project details func (pmm *Client) GetProject(projectSFID string) (*models.ProjectOutputDetailed, error) { + f := logrus.Fields{ + "functionName": "v2.project-service.client.GetProject", + "projectSFID": projectSFID, + "apiGWHost": apiGWHost, + } + + // Lookup in cache first + mutex.Lock() // exclusive lock to the shared project service model map + existingModel, exists := projectServiceModels[projectSFID] + mutex.Unlock() + + if exists { + //log.WithFields(f).Debugf("cache hit - cache size: %d", len(projectServiceModels)) + return existingModel, nil + } + log.WithFields(f).Debugf("cache miss - cache size: %d", len(projectServiceModels)) + tok, err := token.GetToken() if err != nil { return nil, err } clientAuth := runtimeClient.BearerToken(tok) - return pmm.getProject(projectSFID, clientAuth) + + // Lookup the project + log.WithFields(f).Debugf("cache miss - looking up project in the service for: %s...", projectSFID) + projectModel, err := pmm.getProject(projectSFID, clientAuth) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project in the project service for: %s", projectSFID) + return nil, err + } + + // Update our cache for next time + mutex.Lock() // exclusive lock to the shared project service model map + projectServiceModels[projectSFID] = projectModel + log.WithFields(f).Debugf("added project model to cache - cache size: %d", len(projectServiceModels)) + mutex.Unlock() + + return projectModel, nil } // GetProjectByName returns project details for the associated project name -func (pmm *Client) GetProjectByName(projectName string) (*models.ProjectListSearch, error) { +func (pmm *Client) GetProjectByName(ctx context.Context, projectName string) (*models.ProjectListSearch, error) { + f := logrus.Fields{ + "functionName": "v2.project-service.client.GetProjectByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectName": projectName, + "apiGWHost": apiGWHost, + } tok, err := token.GetToken() if err != nil { + log.WithFields(f).WithError(err).Warning("problem retrieving token") return nil, err } + clientAuth := runtimeClient.BearerToken(tok) result, err := pmm.cl.Project.SearchProjects(&project.SearchProjectsParams{ - Name: []string{projectName}, + Name: []string{projectName}, + Context: ctx, }, clientAuth) if err != nil { + log.WithFields(f).WithError(err).Warning("problem searching projects by name") return nil, err } + return result.Payload, nil } // GetParentProject returns the parent project SFID if there is a parent, otherwise returns the provided projectSFID func (pmm *Client) GetParentProject(projectSFID string) (string, error) { f := logrus.Fields{ - "functionName": "getParentProject", + "functionName": "v2.project-service.client.GetParentProject", + "projectSFID": projectSFID, + "apiGWHost": apiGWHost, + } + + // Use our helper function to find the parent, if it exists + parentModel, err := pmm.GetParentProjectModel(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Debugf("unable to lookup parentProjectModel using projectSFID: '%s'", projectSFID) + return "", err + } + if parentModel == nil { + log.WithFields(f).Debugf("unable to lookup parentProjectModel using projectSFID: '%s' - parent project model is nil", projectSFID) + return "", err + } + + return parentModel.ID, nil +} + +// GetParentProjectModel returns the parent project model if there is a parent, otherwise returns nil +func (pmm *Client) GetParentProjectModel(projectSFID string) (*models.ProjectOutputDetailed, error) { + f := logrus.Fields{ + "functionName": "v2.project-service.client.GetParentProjectModel", "projectSFID": projectSFID, + "apiGWHost": apiGWHost, } - log.WithFields(f).Debug("looking up projectModel in SF by projectSFID") + // Lookup in cache first + var exists bool + var existingModel *models.ProjectOutputDetailed + var existingParentModel *models.ProjectOutputDetailed + + // Current project in the cache? + mutex.Lock() // exclusive lock to the shared project service model map + existingModel, exists = projectServiceModels[projectSFID] + mutex.Unlock() + if exists { + //log.WithFields(f).Debugf("cache hit - cache size: %d", len(projectServiceModels)) + + if !utils.IsProjectHaveParent(existingModel) { + //log.WithFields(f).Debugf("project %+v does not have a parent", existingModel) + return nil, nil + } + log.WithFields(f).Debugf("project %+v has a parent", existingModel) + + // Grab the parent ID once + projectParentSFID := utils.GetProjectParentSFID(existingModel) + if projectParentSFID == "" { + log.WithFields(f).Debugf("unable to determine project %+v parent", existingModel) + return nil, nil + } + + // Parent SFID in the cache? + mutex.Lock() // exclusive lock to the shared project service model map + existingParentModel, exists = projectServiceModels[projectParentSFID] + mutex.Unlock() + if exists { + return existingParentModel, nil + } + + // Parent project not in the cache - lookup + parentProjectModel, err := pmm.GetProject(projectParentSFID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup parentProjectModel with projectSFID: '%s'", projectParentSFID) + return nil, err + } + + if parentProjectModel == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup parentProjectModel with projectSFID: '%s' - project model is nil", projectParentSFID) + return nil, nil + } + + // Save/Update our cache for next time + mutex.Lock() // exclusive lock to the shared project service model map + projectServiceModels[projectParentSFID] = parentProjectModel + log.WithFields(f).Debugf("added project model to cache - cache size: %d", len(projectServiceModels)) + mutex.Unlock() + + return parentProjectModel, nil + } + + log.WithFields(f).Debugf("cache miss - looking up projectModel in projectSFID: %s", projectSFID) projectModel, err := pmm.GetProject(projectSFID) if err != nil { log.WithFields(f).Warnf("unable to lookup projectModel in projectModel service by projectSFID, error: %+v", err) - return "", err + return nil, err + } + if projectModel == nil { + return nil, nil + } + + // Save/Update our cache for next time + mutex.Lock() // exclusive lock to the shared project service model map + projectServiceModels[projectSFID] = projectModel + log.WithFields(f).Debugf("added project model to cache - cache size: %d", len(projectServiceModels)) + mutex.Unlock() + + // No parent + if !utils.IsProjectHaveParent(projectModel) { + //log.WithFields(f).Debugf("project %+v does not have a parent", projectModel) + return nil, nil } - // Do they have a parent? - if projectModel.Parent == "" || projectModel.Parent == utils.TheLinuxFoundation { - log.WithFields(f).Debugf("no parent for projectSFID or %s is the parent...", utils.TheLinuxFoundation) - return projectSFID, nil + // Is the parent one of the root parents? + if projectModel.Foundation.Name == utils.TheLinuxFoundation { + log.WithFields(f).Debugf("no parent for projectSFID or %s is the parent...", utils.TheLinuxFoundation) + return nil, nil + } + + // Grab the parent ID once + projectParentSFID := utils.GetProjectParentSFID(projectModel) + if projectParentSFID == "" { + log.WithFields(f).Debugf("unable to determine project %+v parent", projectModel) + return nil, nil + } + + // Parent in the cache? + mutex.Lock() // exclusive lock to the shared project service model map + existingParentModel, exists = projectServiceModels[projectParentSFID] + mutex.Unlock() // exclusive lock to the shared project service model map + if exists { + return existingParentModel, nil + } + + // Parent project not in the cache - lookup + parentProjectModel, err := pmm.GetProject(projectParentSFID) + if err != nil { + log.WithFields(f).WithError(err).Debugf("unable to lookup parentProjectModel with projectSFID: '%s'", projectParentSFID) + return nil, err + } + if parentProjectModel == nil { + log.WithFields(f).WithError(err).Debugf("unable to lookup parentProjectModel with projectSFID: '%s' - project model is nil", projectParentSFID) + return nil, nil } - log.WithFields(f).Debugf("returning parent projectSFID: %s", projectModel.Parent) - return projectModel.Parent, nil + // Save/Update our cache for next time + mutex.Lock() // exclusive lock to the shared project service model map + projectServiceModels[projectParentSFID] = parentProjectModel + log.WithFields(f).Debugf("added project model to cache - cache size: %d", len(projectServiceModels)) + mutex.Unlock() // exclusive lock to the shared project service model map + + return parentProjectModel, nil } // IsTheLinuxFoundation returns true if the specified project SFID is the The Linux Foundation project func (pmm *Client) IsTheLinuxFoundation(projectSFID string) (bool, error) { f := logrus.Fields{ - "functionName": "IsTheLinuxFoundation", + "functionName": "v2.project-service.client.IsTheLinuxFoundation", + "projectSFID": projectSFID, + "apiGWHost": apiGWHost, } - log.WithFields(f).Debug("querying project...") projectModel, err := pmm.GetProject(projectSFID) if err != nil { log.WithFields(f).Warnf("unable to lookup project by ID: %s error: %+v", projectSFID, err) @@ -128,7 +304,7 @@ func (pmm *Client) IsTheLinuxFoundation(projectSFID string) (bool, error) { if projectModel.Name == utils.TheLinuxFoundation { // Save into our cache for next time - log.WithFields(f).Debug("project is the linux foundation...") + log.WithFields(f).Debugf("project is %s ", utils.TheLinuxFoundation) return true, nil } @@ -138,7 +314,9 @@ func (pmm *Client) IsTheLinuxFoundation(projectSFID string) (bool, error) { // IsParentTheLinuxFoundation returns true if the parent is the The Linux Foundation project func (pmm *Client) IsParentTheLinuxFoundation(projectSFID string) (bool, error) { f := logrus.Fields{ - "functionName": "IsParentTheLinuxFoundation", + "functionName": "v2.project-service.client.IsParentTheLinuxFoundation", + "projectSFID": projectSFID, + "apiGWHost": apiGWHost, } log.WithFields(f).Debug("querying project...") @@ -148,19 +326,19 @@ func (pmm *Client) IsParentTheLinuxFoundation(projectSFID string) (bool, error) return false, err } - if projectModel.Parent == "" { + if !utils.IsProjectHaveParent(projectModel) { return false, nil } - parentProjectModel, err := pmm.GetProject(projectModel.Parent) + parentProjectModel, err := pmm.GetProject(projectModel.Foundation.ID) if err != nil { - log.WithFields(f).Warnf("unable to lookup parent project by ID: %s error: %+v", projectModel.Parent, err) + log.WithFields(f).Warnf("unable to lookup parent project by ID: %s error: %+v", projectModel.Foundation.ID, err) return false, err } if parentProjectModel.Name == utils.TheLinuxFoundation { // Save into our cache for next time - log.WithFields(f).Debug("parent project is the linux foundation...") + log.WithFields(f).Debugf("parent project is %s ...", utils.TheLinuxFoundation) return true, nil } @@ -168,56 +346,101 @@ func (pmm *Client) IsParentTheLinuxFoundation(projectSFID string) (bool, error) } // EnableCLA enables CLA service in project-service -func (pmm *Client) EnableCLA(projectSFID string) error { +func (pmm *Client) EnableCLA(ctx context.Context, projectSFID string) error { + f := logrus.Fields{ + "functionName": "v2.project-service.client.EnableCLA", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "apiGWHost": apiGWHost, + } + + theLF, lookupErr := pmm.IsTheLinuxFoundation(projectSFID) + if lookupErr != nil { + log.WithFields(f).WithError(lookupErr).Warnf("unable to test if project is The Linux Foundation using projectSFID: %s", projectSFID) + return lookupErr + } + if theLF { + msg := fmt.Sprintf("unable to set the enabled CLA services for The Linux Foundation with projectSFID: %s - not allowed", projectSFID) + log.WithFields(f).Debug(msg) + return errors.New(msg) + } + tok, err := token.GetToken() if err != nil { + log.WithFields(f).WithError(err).Warning("problem retrieving token") return err } clientAuth := runtimeClient.BearerToken(tok) + projectDetails, err := pmm.getProject(projectSFID, clientAuth) if err != nil { + log.WithFields(f).WithError(err).Warning("problem retrieving project by SFID") return err } + for _, serviceName := range projectDetails.EnabledServices { if serviceName == CLA { // CLA already enabled return nil } } + enabledServices := projectDetails.EnabledServices enabledServices = append(enabledServices, CLA) - return pmm.updateEnabledServices(projectSFID, enabledServices, clientAuth) + return pmm.updateEnabledServices(ctx, projectSFID, enabledServices, clientAuth) } -func (pmm *Client) updateEnabledServices(projectSFID string, enabledServices []string, clientAuth runtime.ClientAuthInfoWriter) error { +func (pmm *Client) updateEnabledServices(ctx context.Context, projectSFID string, enabledServices []string, clientAuth runtime.ClientAuthInfoWriter) error { + f := logrus.Fields{ + "functionName": "v2.project-service.client.updateEnabledServices", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "enabledServices": enabledServices, + "apiGWHost": apiGWHost, + } + params := project.NewUpdateProjectParams() params.ProjectID = projectSFID if len(enabledServices) == 0 { enabledServices = append(enabledServices, NA) } + params.Body = &models.ProjectInput{ ProjectCommon: models.ProjectCommon{ EnabledServices: enabledServices, }, } + _, err := pmm.cl.Project.UpdateProject(params, clientAuth) //nolint if err != nil { - return err + log.WithFields(f).WithError(err).Warnf("problem updating project enabled services") } + return err } // DisableCLA enables CLA service in project-service -func (pmm *Client) DisableCLA(projectSFID string) error { +func (pmm *Client) DisableCLA(ctx context.Context, projectSFID string) error { + f := logrus.Fields{ + "functionName": "v2.project-service.client.DisableCLA", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "apiGWHost": apiGWHost, + } + tok, err := token.GetToken() if err != nil { + log.WithFields(f).WithError(err).Warning("problem retrieving token") return err } clientAuth := runtimeClient.BearerToken(tok) + projectDetails, err := pmm.getProject(projectSFID, clientAuth) if err != nil { + log.WithFields(f).WithError(err).Warning("problem retrieving project by SFID") return err } + newEnabledServices := make([]string, 0) var claFound bool for _, serviceName := range projectDetails.EnabledServices { @@ -231,5 +454,75 @@ func (pmm *Client) DisableCLA(projectSFID string) error { // CLA already disabled return nil } - return pmm.updateEnabledServices(projectSFID, newEnabledServices, clientAuth) + return pmm.updateEnabledServices(ctx, projectSFID, newEnabledServices, clientAuth) +} + +// GetSummary gets projects tree hierarchy and project details +func (pmm *Client) GetSummary(ctx context.Context, projectSFID string) ([]*models.ProjectSummary, error) { + f := logrus.Fields{ + "functionName": "v2.project-service.client.Summary", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": projectSFID, + } + + tok, err := token.GetToken() + if err != nil { + return nil, err + } + + clientAuth := runtimeClient.BearerToken(tok) + + filter := fmt.Sprintf("id eq %s", projectSFID) + log.WithFields(f).Debugf("Getting project summary for :%s ", projectSFID) + view := "pcc" + offsetDefault := int64(0) + orderByDefault := string("createddate") + pageSizeDefault := int64(100) + + params := &project.GetSummaryParams{ + DollarFilter: &filter, + MyProjects: nil, + Offset: &offsetDefault, + OrderBy: &orderByDefault, + PageSize: &pageSizeDefault, + View: &view, + Context: ctx, // must set for the GetSummary API call, otherwise we get a Err:context.deadlineExceededError{} + } + + result, err := pmm.cl.Project.GetSummary(params, clientAuth) + + if err != nil { + log.WithFields(f).WithError(err).Debugf("unable to query project summary for : %s , error: %+v ", projectSFID, err) + return nil, err + } + + return result.Payload.Data, nil +} + +// IsAnyProjectTheRootParent returns true if one or more of the project ID's in the list is one of the root parents, returns false otherwise +func (pmm *Client) IsAnyProjectTheRootParent(sliceProjectSFID []string) bool { + var retVal bool + + // Check each project to see if it is one of the root parents + for _, projectSFID := range sliceProjectSFID { + // If so, return true, we're done + if isTLF, err := pmm.IsTheLinuxFoundation(projectSFID); isTLF && err == nil { + retVal = isTLF + break + } + } + + return retVal +} + +// RemoveLinuxFoundationParentsFromProjectList removes any Linux Foundation root/parent projects from the list +func (pmm *Client) RemoveLinuxFoundationParentsFromProjectList(projectList []string) []string { + var filteredProjectList []string + for _, projectSFID := range projectList { + // If not one of our Linux Foundation root/parent projects, then add it to the list + if isTLF, err := pmm.IsTheLinuxFoundation(projectSFID); !isTLF && err == nil { + filteredProjectList = append(filteredProjectList, projectSFID) + } + } + return filteredProjectList } diff --git a/cla-backend-go/v2/project/converters.go b/cla-backend-go/v2/project/converters.go index 00248dbef..781ec67f2 100644 --- a/cla-backend-go/v2/project/converters.go +++ b/cla-backend-go/v2/project/converters.go @@ -4,7 +4,7 @@ package project 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/project/handlers.go b/cla-backend-go/v2/project/handlers.go index 74aeb669d..369c7c165 100644 --- a/cla-backend-go/v2/project/handlers.go +++ b/cla-backend-go/v2/project/handlers.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + v1Project "github.com/communitybridge/easycla/cla-backend-go/project/service" + projectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" v2ProjectServiceModels "github.com/communitybridge/easycla/cla-backend-go/v2/project-service/models" @@ -19,21 +21,22 @@ import ( "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/events" - v1ProjectOps "github.com/communitybridge/easycla/cla-backend-go/gen/restapi/operations/project" + v1ProjectOps "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/project" "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/project" log "github.com/communitybridge/easycla/cla-backend-go/logging" - v1Project "github.com/communitybridge/easycla/cla-backend-go/project" "github.com/go-openapi/runtime/middleware" ) // Configure establishes the middleware handlers for the project service func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service Service, eventsService events.Service) { //nolint // Get Projects - api.ProjectGetProjectsHandler = project.GetProjectsHandlerFunc(func(params project.GetProjectsParams, user *auth.User) middleware.Responder { + api.ProjectGetProjectsHandler = project.GetProjectsHandlerFunc(func(params project.GetProjectsParams, 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) + // No auth checks - anyone can request the list of projects projects, err := service.GetCLAGroups(ctx, &v1ProjectOps.GetProjectsParams{ HTTPRequest: params.HTTPRequest, @@ -56,10 +59,18 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service }) // Get Project By ID - api.ProjectGetProjectByIDHandler = project.GetProjectByIDHandlerFunc(func(params project.GetProjectByIDParams, user *auth.User) middleware.Responder { + api.ProjectGetProjectByIDHandler = project.GetProjectByIDHandlerFunc(func(params project.GetProjectByIDParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.project.handlers.ProjectGetProjectByIDHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": params.ProjectSfdcID, + "userEmail": authUser.Email, + "userName": authUser.UserName, + } + claGroupModel, err := service.GetCLAGroupByID(ctx, params.ProjectSfdcID) if err != nil { @@ -73,34 +84,39 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service return project.NewGetProjectByIDNotFound().WithXRequestID(reqID) } - if !utils.IsUserAuthorizedForProjectTree(user, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { - return project.NewGetProjectByIDForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Get Project By ID with Project scope of %s", - user.UserName, claGroupModel.ProjectExternalID), - XRequestID: reqID, - }) + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user '%s' does not have access to Get Project By ID with Project scope of %s", + authUser.UserName, claGroupModel.ProjectExternalID) + return project.NewGetProjectByIDForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } result, err := v2ProjectModel(claGroupModel) if err != nil { - return project.NewGetProjectByIDInternalServerError().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) + msg := fmt.Sprintf("unable to convert CLA Group '%s' with ID: '%s' to a response model", claGroupModel.ProjectName, claGroupModel.ProjectID) + log.WithFields(f).WithError(err).Warn(msg) + return project.NewGetProjectByIDInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) } return project.NewGetProjectByIDOK().WithXRequestID(reqID).WithPayload(result) }) - api.ProjectGetProjectsByExternalIDHandler = project.GetProjectsByExternalIDHandlerFunc(func(params project.GetProjectsByExternalIDParams, user *auth.User) middleware.Responder { + api.ProjectGetProjectsByExternalIDHandler = project.GetProjectsByExternalIDHandlerFunc(func(params project.GetProjectsByExternalIDParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) - if !utils.IsUserAuthorizedForProjectTree(user, params.ExternalID, utils.ALLOW_ADMIN_SCOPE) { - return project.NewGetProjectsByExternalIDForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Get Projects By External ID with Project scope of %s", - user.UserName, params.ExternalID), - XRequestID: reqID, - }) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.project.handlers.ProjectGetProjectsByExternalIDHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "externalID": params.ExternalID, + "userEmail": authUser.Email, + "userName": authUser.UserName, + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user '%s' does not have access to Get Projects By External ID with Project scope of '%s'", + authUser.UserName, params.ExternalID) + log.WithFields(f).Debug(msg) + return project.NewGetProjectsByExternalIDForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } claGroupModel, err := service.GetCLAGroupsByExternalID(ctx, &v1ProjectOps.GetProjectsByExternalIDParams{ @@ -119,20 +135,25 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service return project.NewGetProjectsByExternalIDInternalServerError().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } if results.Projects == nil { - return project.NewGetProjectsByExternalIDNotFound().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "404", - Message: fmt.Sprintf("project not found with id. [%s]", params.ExternalID), - XRequestID: reqID, - }) + msg := fmt.Sprintf("project not found with id: '%s]", params.ExternalID) + log.WithFields(f).Debug(msg) + return project.NewGetProjectsByExternalIDNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) } return project.NewGetProjectsByExternalIDOK().WithXRequestID(reqID).WithPayload(results) }) // Get Project By Name - api.ProjectGetProjectByNameHandler = project.GetProjectByNameHandlerFunc(func(params project.GetProjectByNameParams, user *auth.User) middleware.Responder { + api.ProjectGetProjectByNameHandler = project.GetProjectByNameHandlerFunc(func(params project.GetProjectByNameParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint - utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + f := logrus.Fields{ + "functionName": "v2.project.handlers.ProjectGetProjectByNameHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectName": params.ProjectName, + "userEmail": authUser.Email, + "userName": authUser.UserName, + } claGroupModel, err := service.GetCLAGroupByName(ctx, params.ProjectName) if err != nil { @@ -142,13 +163,11 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service return project.NewGetProjectByNameNotFound().WithXRequestID(reqID) } - if !utils.IsUserAuthorizedForProjectTree(user, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { - return project.NewGetProjectByNameForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Get Project By Name with Project scope of %s", - user.UserName, claGroupModel.ProjectExternalID), - XRequestID: reqID, - }) + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user '%s' does not have access to Get Projects By Name with Project scope of '%s'", + authUser.UserName, claGroupModel.ProjectExternalID) + log.WithFields(f).Debug(msg) + return project.NewGetProjectByNameForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } result, err := v2ProjectModel(claGroupModel) @@ -159,18 +178,18 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service }) // Delete Project By ID - api.ProjectDeleteProjectByIDHandler = project.DeleteProjectByIDHandlerFunc(func(params project.DeleteProjectByIDParams, user *auth.User) middleware.Responder { + api.ProjectDeleteProjectByIDHandler = project.DeleteProjectByIDHandlerFunc(func(params project.DeleteProjectByIDParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "ProjectDeleteProjectByIDHandler", + "functionName": "v2.project.handlers.ProjectDeleteProjectByIDHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSfdcID, - "userEmail": user.Email, - "userName": user.UserName, + "userEmail": authUser.Email, + "userName": authUser.UserName, } log.WithFields(f).Debug("Processing delete request") - utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) claGroupModel, err := service.GetCLAGroupByID(ctx, params.ProjectSfdcID) if err != nil { if err == ErrCLAGroupDoesNotExist { @@ -179,13 +198,11 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service return project.NewDeleteProjectByIDBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } - if !utils.IsUserAuthorizedForProjectTree(user, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { - return project.NewDeleteProjectByIDForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Delete Project By ID with Project scope of %s", - user.UserName, claGroupModel.ProjectExternalID), - XRequestID: reqID, - }) + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user '%s' does not have access to Delete Project By ID with Project scope of %s", + authUser.UserName, claGroupModel.ProjectExternalID) + log.WithFields(f).Debug(msg) + return project.NewDeleteProjectByIDForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } err = service.DeleteCLAGroup(ctx, params.ProjectSfdcID) @@ -198,7 +215,8 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service eventsService.LogEvent(&events.LogEventArgs{ EventType: events.CLAGroupDeleted, ClaGroupModel: claGroupModel, - LfUsername: user.UserName, + ProjectSFID: params.ProjectSfdcID, + LfUsername: authUser.UserName, EventData: &events.CLAGroupDeletedEventData{}, }) @@ -217,7 +235,7 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service } return project.NewUpdateProjectNotFound().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } - if !utils.IsUserAuthorizedForProjectTree(user, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { + if !utils.IsUserAuthorizedForProjectTree(ctx, user, claGroupModel.ProjectExternalID, utils.ALLOW_ADMIN_SCOPE) { return project.NewUpdateProjectForbidden().WithXRequestID(reqID).WithPayload(&models.ErrorResponse{ Code: "403", Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Update Project By ID with Project scope of %s", @@ -239,14 +257,24 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service return project.NewUpdateProjectBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(reqID, err)) } + eventData := &events.CLAGroupUpdatedEventData{ + OldClaGroupName: claGroupModel.ProjectName, + OldClaGroupDescription: claGroupModel.ProjectDescription, + } + + if in.ProjectName != "" { + eventData.NewClaGroupName = in.ProjectName + } + + if in.ProjectDescription != "" { + eventData.NewClaGroupDescription = in.ProjectDescription + } + eventsService.LogEvent(&events.LogEventArgs{ EventType: events.CLAGroupUpdated, ClaGroupModel: claGroupModel, LfUsername: user.UserName, - EventData: &events.CLAGroupUpdatedEventData{ - ClaGroupName: claGroupModel.ProjectName, - ClaGroupDescription: claGroupModel.ProjectDescription, - }, + EventData: eventData, }) result, err := v2ProjectModel(claGroupModel) @@ -273,7 +301,7 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service reqID := utils.GetRequestID(params.XREQUESTID) ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint f := logrus.Fields{ - "functionName": "ProjectGetSFProjectInfoByIDHandler", + "functionName": "v2.project.handlers.ProjectGetSFProjectInfoByIDHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": params.ProjectSFID, "userEmail": user.Email, @@ -290,10 +318,10 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service // Lookup the parent info, if it's available var parentName string - if sfProject.Parent != "" { - sfParentProject, err := psc.GetProject(sfProject.Parent) + if utils.IsProjectHaveParent(sfProject) { + sfParentProject, err := psc.GetProject(utils.GetProjectParentSFID(sfProject)) if err != nil { - log.WithFields(f).WithError(err).Warnf("unable to load parant project by ID: %s", sfProject.Parent) + log.WithFields(f).WithError(err).Warnf("unable to load parant project by ID: %s", utils.GetProjectParentSFID(sfProject)) } if sfParentProject != nil { @@ -308,17 +336,17 @@ func Configure(api *operations.EasyclaAPI, service v1Project.Service, v2Service func buildSFProjectSummary(sfProject *v2ProjectServiceModels.ProjectOutputDetailed, parentName string) *models.SfProjectSummary { return &models.SfProjectSummary{ - EntityName: sfProject.EntityName, + EntityName: utils.StringValue(sfProject.EntityName), EntityType: sfProject.EntityType, - Funding: sfProject.Funding, + Funding: *sfProject.Funding, ID: sfProject.ID, LfSupported: sfProject.LFSponsored, Name: sfProject.Name, - ParentID: sfProject.Parent, + ParentID: utils.GetProjectParentSFID(sfProject), ParentName: parentName, Slug: sfProject.Slug, Status: sfProject.Status, Type: sfProject.Type, - IsStandalone: (sfProject.Type != utils.ProjectTypeProjectGroup) && (sfProject.Parent == "" || sfProject.Parent == utils.TheLinuxFoundation), + IsStandalone: utils.IsStandaloneProject(sfProject), } } diff --git a/cla-backend-go/v2/project/service.go b/cla-backend-go/v2/project/service.go index 09189dcb4..63054266c 100644 --- a/cla-backend-go/v2/project/service.go +++ b/cla-backend-go/v2/project/service.go @@ -8,6 +8,9 @@ import ( "sort" "sync" + "github.com/communitybridge/easycla/cla-backend-go/project/repository" + v1Project "github.com/communitybridge/easycla/cla-backend-go/project/service" + "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/sirupsen/logrus" @@ -17,7 +20,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" log "github.com/communitybridge/easycla/cla-backend-go/logging" - v1Project "github.com/communitybridge/easycla/cla-backend-go/project" v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" ) @@ -29,12 +31,12 @@ type Service interface { // service type service struct { v1ProjectService v1Project.Service - projectRepo v1Project.ProjectRepository + projectRepo repository.ProjectRepository projectsClaGroups projects_cla_groups.Repository } // NewService returns an instance of v2 project service -func NewService(v1ProjectService v1Project.Service, projectRepo v1Project.ProjectRepository, pcgRepo projects_cla_groups.Repository) Service { +func NewService(v1ProjectService v1Project.Service, projectRepo repository.ProjectRepository, pcgRepo projects_cla_groups.Repository) Service { return &service{ v1ProjectService: v1ProjectService, projectRepo: projectRepo, @@ -50,7 +52,7 @@ func (s *service) GetCLAProjectsByID(ctx context.Context, foundationSFID string) } enabledClas := make([]*models.EnabledCla, 0) - claGroupsMapping, err := s.projectsClaGroups.GetProjectsIdsForFoundation(foundationSFID) + claGroupsMapping, err := s.projectsClaGroups.GetProjectsIdsForFoundation(ctx, foundationSFID) if err != nil { return nil, err } @@ -83,7 +85,7 @@ func (s *service) GetCLAProjectsByID(ctx context.Context, foundationSFID string) cla.FoundationSfid = foundationSFID } - claGroup, err := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, v1Project.DontLoadRepoDetails) + claGroup, err := s.projectRepo.GetCLAGroupByID(ctx, claGroupID, repository.DontLoadRepoDetails) if err != nil { log.WithFields(f).Warnf("unable to fetch cla-group details of %s", claGroupID) } else { diff --git a/cla-backend-go/v2/repositories/gitlab_services.go b/cla-backend-go/v2/repositories/gitlab_services.go new file mode 100644 index 000000000..dfaebce45 --- /dev/null +++ b/cla-backend-go/v2/repositories/gitlab_services.go @@ -0,0 +1,481 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package repositories + +import ( + "context" + "errors" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + "github.com/communitybridge/easycla/cla-backend-go/events" + + v2GitLabOrg "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + gitLabApi "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/sirupsen/logrus" + + v2Models "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + repoModels "github.com/communitybridge/easycla/cla-backend-go/repositories" +) + +// GitLabAddRepositories add a lst of GitLab repositories to the collection - default is not enabled/used/active by a CLA Group +func (s *Service) GitLabAddRepositories(ctx context.Context, projectSFID string, input *GitLabAddRepoModel) (*v2Models.GitlabRepositoriesList, error) { + return s.GitLabAddRepositoriesWithEnabledFlag(ctx, projectSFID, input, false) +} + +// GitLabAddRepositoriesWithEnabledFlag add a lst of GitLab repositories to the collection +func (s *Service) GitLabAddRepositoriesWithEnabledFlag(ctx context.Context, projectSFID string, input *GitLabAddRepoModel, enabled bool) (*v2Models.GitlabRepositoriesList, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.gitlab_services.GitLabAddRepositories", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "groupName": input.GroupName, + "claGroupID": input.ClaGroupID, + "groupFullPath": input.GroupFullPath, + "groupID": input.ExternalID, + } + + var gitLabOrgModel *common.GitLabOrganization + var getOrgErr error + if input.GroupName != "" { + log.WithFields(f).Debugf("fetching GitLab group/organization by name: %s", input.GroupName) + gitLabOrgModel, getOrgErr = s.glOrgRepo.GetGitLabOrganizationByName(ctx, input.GroupName) + if getOrgErr != nil { + msg := fmt.Sprintf("problem loading GitLab group/organization by name: %s, error: %v", input.GroupName, getOrgErr) + log.WithFields(f).WithError(getOrgErr).Warn(msg) + return nil, errors.New(msg) + } + } else if input.GroupFullPath != "" { + log.WithFields(f).Debugf("fetching GitLab group/organization by full path: %s", input.GroupFullPath) + gitLabOrgModel, getOrgErr = s.glOrgRepo.GetGitLabOrganizationByFullPath(ctx, input.GroupFullPath) + if getOrgErr != nil { + msg := fmt.Sprintf("problem loading GitLab group/organization by full path: %s, error: %v", input.GroupFullPath, getOrgErr) + log.WithFields(f).WithError(getOrgErr).Warn(msg) + return nil, errors.New(msg) + } + } + if gitLabOrgModel == nil { + msg := fmt.Sprintf("problem loading GitLab group/organization by name '%s' or full path '%s'", input.GroupName, input.GroupFullPath) + log.WithFields(f).Warn(msg) + return nil, errors.New(msg) + } + log.WithFields(f).Debugf("successfully loaded GitLab group/organization") + + // Get the client + gitLabClient, err := gitLabApi.NewGitlabOauthClient(gitLabOrgModel.AuthInfo, s.gitLabApp) + if err != nil { + return nil, fmt.Errorf("initializing GitLab client : %v", err) + } + + type GitLabAddRepositoryResponse struct { + RepositoryName string + RepositoryFullPath string + Error error + } + addRepoRespChan := make(chan *GitLabAddRepositoryResponse, len(input.ProjectIDList)) + + // Add each repo - could be a lot of repos, so we run this in a go routine + for _, gitLabProjectID := range input.ProjectIDList { + go func(gitLabProjectID int) { + log.WithFields(f).Debugf("loading GitLab project from GitLab using projectID: %d...", gitLabProjectID) + project, getProjectErr := gitLabApi.GetProjectByID(ctx, gitLabClient, gitLabProjectID) + if getProjectErr != nil { + newErr := fmt.Errorf("unable to load GitLab project using ID: %d, error: %v", gitLabProjectID, getProjectErr) + log.WithFields(f).WithError(newErr) + addRepoRespChan <- &GitLabAddRepositoryResponse{ + Error: newErr, + } + return + } + log.WithFields(f).Debugf("loaded GitLab project from GitLab using projectID: %d", gitLabProjectID) + + // Convert int to string + repositoryExternalIDString := strconv.Itoa(project.ID) + + inputDBModel := &repoModels.RepositoryDBModel{ + RepositorySfdcID: projectSFID, + ProjectSFID: projectSFID, + RepositoryExternalID: repositoryExternalIDString, + RepositoryName: project.PathWithNamespace, // Name column is actually the full path for both GitHub and GitLab + RepositoryFullPath: project.PathWithNamespace, + RepositoryURL: project.WebURL, + RepositoryOrganizationName: input.GroupName, + RepositoryCLAGroupID: input.ClaGroupID, + RepositoryType: utils.GitLabLower, // should always be gitlab + Enabled: enabled, + } + + repoModel, addErr := s.gitV2Repository.GitLabAddRepository(ctx, projectSFID, inputDBModel) + if addErr != nil || repoModel == nil { + log.WithFields(f).WithError(addErr).Warnf("problem adding GitLab repository with name: %s, error: %+v", project.PathWithNamespace, addErr) + addRepoRespChan <- &GitLabAddRepositoryResponse{ + Error: addErr, + } + return + } + + // Log the event + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryAdded, + ProjectSFID: projectSFID, + CLAGroupID: input.ClaGroupID, + LfUsername: utils.GetUserNameFromContext(ctx), + EventData: &events.RepositoryAddedEventData{ + RepositoryName: project.PathWithNamespace, // give the full path/name + }, + }) + addRepoRespChan <- &GitLabAddRepositoryResponse{ + RepositoryName: repoModel.RepositoryName, + RepositoryFullPath: repoModel.RepositoryFullPath, + Error: nil, + } + }(int(gitLabProjectID)) // ok to down cast + } + + // Wait for the go routines to finish and load up the results + log.WithFields(f).Debug("waiting for add repos to finish...") + var lastErr error + for range input.ProjectIDList { + select { + case response := <-addRepoRespChan: + if response.Error != nil { + log.WithFields(f).WithError(response.Error).Warn(response.Error.Error()) + lastErr = response.Error + } else { + log.WithFields(f).Debugf("added repo: %s with full path: %s", response.RepositoryName, response.RepositoryFullPath) + } + case <-ctx.Done(): + log.WithFields(f).WithError(ctx.Err()).Warnf("waiting for add repositories timed out") + lastErr = fmt.Errorf("add repositories failed with timeout, error: %v", ctx.Err()) + } + } + + if lastErr != nil { + return nil, lastErr + } + + return s.GitLabGetRepositoriesByProjectSFID(ctx, projectSFID) +} + +// GitLabAddRepositoriesByApp adds the GitLab repositories based on the application credentials +func (s *Service) GitLabAddRepositoriesByApp(ctx context.Context, gitLabOrgModel *v2GitLabOrg.GitLabOrganization) ([]*v2Models.GitlabRepository, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.gitlab_services.GitLabAddRepositoriesByApp", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": gitLabOrgModel.ProjectSFID, + "organizationName": gitLabOrgModel.OrganizationName, + "groupFullPath": gitLabOrgModel.OrganizationFullPath, + "groupID": gitLabOrgModel.ExternalGroupID, + } + + // Get the client + gitLabClient, err := gitLabApi.NewGitlabOauthClient(gitLabOrgModel.AuthInfo, s.gitLabApp) + if err != nil { + return nil, fmt.Errorf("initializing gitlab client : %v", err) + } + + // lookup CLA Group for this project SFID + projectCLAGroupModel, projectCLAGroupLookupErr := s.projectsClaGroupsRepo.GetClaGroupIDForProject(ctx, gitLabOrgModel.ProjectSFID) + if projectCLAGroupLookupErr != nil || projectCLAGroupModel == nil { + return nil, fmt.Errorf("unable to locate Project CLAGroup using projectSFID: %s for GitLab repositories group ID: %d, error: %+v", gitLabOrgModel.ProjectSFID, gitLabOrgModel.ExternalGroupID, projectCLAGroupLookupErr) + } + + // Query the project list by organization name + projectList, projectListErr := gitLabApi.GetGroupProjectListByGroupID(ctx, gitLabClient, gitLabOrgModel.ExternalGroupID) + if projectListErr != nil { + return nil, projectListErr + } + + var listProjectIDs []int64 + + // Build a list of project IDs + for _, proj := range projectList { + // Ensure we only add GitLab projects/repositories that are in the tree path of the GitLab group/organization - we don't want to reach over into a different tree if the user has access + // The GetGroupProjectListByGroupID call in some cases returns more than expected (need to investigate why) + if strings.HasPrefix(proj.PathWithNamespace, gitLabOrgModel.OrganizationFullPath) { + log.WithFields(f).Debugf("adding - id: %d, repo: %s, path: %s, full path: %s, weburl: %s", proj.ID, proj.Name, proj.Path, proj.PathWithNamespace, proj.WebURL) + listProjectIDs = append(listProjectIDs, int64(proj.ID)) + } else { + log.WithFields(f).Debugf("skipping - id: %d, repo: %s, path: %s, full path: %s, weburl: %s", proj.ID, proj.Name, proj.Path, proj.PathWithNamespace, proj.WebURL) + } + } + + // Build input to the add function + input := &GitLabAddRepoModel{ + ClaGroupID: projectCLAGroupModel.ClaGroupID, + GroupName: gitLabOrgModel.OrganizationName, + GroupFullPath: gitLabOrgModel.OrganizationFullPath, + ExternalID: int64(gitLabOrgModel.ExternalGroupID), + ProjectIDList: listProjectIDs, + } + log.WithFields(f).Debugf("adding %d GitLab repositories", len(listProjectIDs)) + _, addRepoErr := s.GitLabAddRepositories(ctx, gitLabOrgModel.ProjectSFID, input) + if addRepoErr != nil { + log.WithFields(f).WithError(addRepoErr).Warnf("problem adding %d GitLab repositories", len(listProjectIDs)) + return nil, addRepoErr + } + + // Return the list of repos to caller + log.WithFields(f).Debugf("fetching complete repository list by project SFID: %s", gitLabOrgModel.ProjectSFID) + dbModels, getRepoErr := s.gitV2Repository.GitHubGetRepositoriesByProjectSFID(ctx, gitLabOrgModel.ProjectSFID) + if getRepoErr != nil { + log.WithFields(f).WithError(getRepoErr).Warnf("problem fetching repositories by project SFID: %s", gitLabOrgModel.ProjectSFID) + return nil, getRepoErr + } + + return dbModelsToGitLabRepositories(dbModels) +} + +// GitLabGetRepository service function +func (s *Service) GitLabGetRepository(ctx context.Context, repositoryID string) (*v2Models.GitlabRepository, error) { + dbModel, err := s.gitV2Repository.GitLabGetRepository(ctx, repositoryID) + if err != nil { + return nil, err + } + + return dbModelToGitLabRepository(dbModel) +} + +// GitLabGetRepositoryByName service function +func (s *Service) GitLabGetRepositoryByName(ctx context.Context, repositoryName string) (*v2Models.GitlabRepository, error) { + dbModel, err := s.gitV2Repository.GitLabGetRepositoryByName(ctx, repositoryName) + if err != nil { + return nil, err + } + + return dbModelToGitLabRepository(dbModel) +} + +// GitLabGetRepositoryByExternalID service function +func (s *Service) GitLabGetRepositoryByExternalID(ctx context.Context, repositoryExternalID int64) (*v2Models.GitlabRepository, error) { + dbModel, err := s.gitV2Repository.GitLabGetRepositoryByExternalID(ctx, repositoryExternalID) + if err != nil { + return nil, err + } + + return dbModelToGitLabRepository(dbModel) +} + +// GitLabGetRepositoriesByProjectSFID service function +func (s *Service) GitLabGetRepositoriesByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabRepositoriesList, error) { + dbModel, err := s.gitV2Repository.GitHubGetRepositoriesByProjectSFID(ctx, projectSFID) + if err != nil { + return nil, err + } + + responses, err := dbModelsToGitLabRepositories(dbModel) + if err != nil { + return nil, err + } + + // sort result by name + sort.Slice(responses, func(i, j int) bool { + return responses[i].RepositoryName < responses[j].RepositoryName + }) + + return &v2Models.GitlabRepositoriesList{ + List: responses, + }, nil +} + +// GitLabGetRepositoriesByCLAGroup service function +func (s *Service) GitLabGetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, enabled bool) (*v2Models.GitlabRepositoriesList, error) { + var dbModels []*repoModels.RepositoryDBModel + var err error + if enabled { + dbModels, err = s.gitV2Repository.GitHubGetRepositoriesByCLAGroupEnabled(ctx, claGroupID) + } else { + dbModels, err = s.gitV2Repository.GitHubGetRepositoriesByCLAGroupDisabled(ctx, claGroupID) + } + if err != nil { + return nil, err + } + + responses, err := dbModelsToGitLabRepositories(dbModels) + if err != nil { + return nil, err + } + + return &v2Models.GitlabRepositoriesList{ + List: responses, + }, nil +} + +// GitLabGetRepositoriesByOrganizationName returns the list of repositories associated with the Organization/Group name +func (s *Service) GitLabGetRepositoriesByOrganizationName(ctx context.Context, orgName string) (*v2Models.GitlabRepositoriesList, error) { + dbModels, err := s.gitV2Repository.GitLabGetRepositoriesByOrganizationName(ctx, orgName) + if err != nil { + return nil, err + } + + responses, err := dbModelsToGitLabRepositories(dbModels) + if err != nil { + return nil, err + } + + return &v2Models.GitlabRepositoriesList{ + List: responses, + }, nil +} + +// GitLabGetRepositoriesByNamePrefix returns a list of repositories that match the name prefix +func (s *Service) GitLabGetRepositoriesByNamePrefix(ctx context.Context, repositoryNamePrefix string) (*v2Models.GitlabRepositoriesList, error) { + dbModels, err := s.gitV2Repository.GitLabGetRepositoriesByNamePrefix(ctx, repositoryNamePrefix) + if err != nil { + return nil, err + } + + responses, err := dbModelsToGitLabRepositories(dbModels) + if err != nil { + return nil, err + } + + return &v2Models.GitlabRepositoriesList{ + List: responses, + }, nil +} + +// GitLabEnrollRepositories assigns repos to a CLA Group +func (s *Service) GitLabEnrollRepositories(ctx context.Context, claGroupID string, repositoryIDList []int64, enrollValue bool) error { + f := logrus.Fields{ + "functionName": "v2.repositories.gitlab_services.GitLabEnrollRepositories", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "enrollValue": enrollValue, + } + + type GitLabUpdateRepositoryResponse struct { + RepositoryID int64 + Error error + } + updateRepoChan := make(chan *GitLabUpdateRepositoryResponse, len(repositoryIDList)) + + for _, repoID := range repositoryIDList { + go func(claGroupID string, repoID int64) { + updateErr := s.GitLabEnrollRepository(ctx, claGroupID, repoID, enrollValue) + updateRepoChan <- &GitLabUpdateRepositoryResponse{ + RepositoryID: repoID, + Error: updateErr, + } + }(claGroupID, repoID) + } + + // Wait for the go routines to finish and load up the results + log.WithFields(f).Debug("waiting for update repos to finish...") + var lastErr error + for range repositoryIDList { + select { + case response := <-updateRepoChan: + if response.Error != nil { + log.WithFields(f).WithError(response.Error).Warn(response.Error.Error()) + lastErr = response.Error + } else { + log.WithFields(f).Debugf("updated repo with ID: %d", response.RepositoryID) + } + case <-ctx.Done(): + log.WithFields(f).WithError(ctx.Err()).Warnf("waiting for update GitLab repos timed out") + lastErr = fmt.Errorf("waiting for update GitLab repositories timed out, error: %v", ctx.Err()) + } + } + + return lastErr +} + +// GitLabEnrollRepository service function enrolls a single GitLab repository to the specified CLA Group +func (s *Service) GitLabEnrollRepository(ctx context.Context, claGroupID string, repositoryExternalID int64, enrollValue bool) error { + return s.gitV2Repository.GitLabEnrollRepositoryByID(ctx, claGroupID, repositoryExternalID, enrollValue) +} + +// GitLabEnrollCLAGroupRepositories service function +func (s *Service) GitLabEnrollCLAGroupRepositories(ctx context.Context, claGroupID string, enrollValue bool) error { + return s.gitV2Repository.GitLabEnableCLAGroupRepositories(ctx, claGroupID, enrollValue) +} + +// GitLabDeleteRepositories deletes the repositories under the specified path +func (s *Service) GitLabDeleteRepositories(ctx context.Context, gitLabGroupPath string) error { + return s.gitV2Repository.GitLabDeleteRepositories(ctx, gitLabGroupPath) +} + +// GitLabDeleteRepositoryByExternalID deletes the specified repository +func (s *Service) GitLabDeleteRepositoryByExternalID(ctx context.Context, gitLabExternalID int64) error { + // Load the record - needed for the event log after we delete + record, getErr := s.gitV2Repository.GitLabGetRepositoryByExternalID(ctx, gitLabExternalID) + if getErr != nil { + return getErr + } + + // Delete the record + err := s.gitV2Repository.GitLabDeleteRepositoryByExternalID(ctx, gitLabExternalID) + if err != nil { + return err + } + + // Convert the external ID value + repositoryExternalID, parseIntErr := strconv.ParseInt(record.RepositoryExternalID, 10, 64) + if err == nil { + return parseIntErr + } + + // Log the event + s.eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryDeleted, + ProjectSFID: record.ProjectSFID, + CLAGroupID: record.RepositoryCLAGroupID, + LfUsername: utils.GetUserNameFromContext(ctx), + EventData: &events.RepositoryDeletedEventData{ + RepositoryName: record.RepositoryFullPath, // give the full path/name + RepositoryExternalID: repositoryExternalID, + }, + }) + return err +} + +// dbModelToGitLabRepository converts the database model to a v2 response model +func dbModelToGitLabRepository(dbModel *repoModels.RepositoryDBModel) (*v2Models.GitlabRepository, error) { + + gitLabExternalID, err := strconv.ParseInt(dbModel.RepositoryExternalID, 10, 64) + if err != nil { + return nil, err + } + + response := v2Models.GitlabRepository{ + RepositoryID: dbModel.RepositoryID, // Internal database ID for this repository record + RepositoryProjectSfid: dbModel.ProjectSFID, // Project SFID + RepositoryClaGroupID: dbModel.RepositoryCLAGroupID, // CLA Group ID + RepositoryExternalID: gitLabExternalID, // GitLab unique gitV1Repository ID + RepositoryName: dbModel.RepositoryName, // Short repository name + RepositoryFullPath: dbModel.RepositoryFullPath, // Repository full path + RepositoryOrganizationName: dbModel.RepositoryOrganizationName, // Group/Organization name + RepositoryURL: dbModel.RepositoryURL, // full url + RepositoryType: dbModel.RepositoryType, // gitlab + Enabled: dbModel.Enabled, // Enabled flag + DateCreated: dbModel.DateCreated, // date created + DateModified: dbModel.DateModified, // date updated + Note: dbModel.Note, // Optional note + Version: dbModel.Version, // record version + } + + return &response, nil +} + +// dbModelsToGitLabRepositories converts the slice of database models to a slice of v2 response model +func dbModelsToGitLabRepositories(dbModels []*repoModels.RepositoryDBModel) ([]*v2Models.GitlabRepository, error) { + var responses []*v2Models.GitlabRepository + for _, dbModel := range dbModels { + response, err := dbModelToGitLabRepository(dbModel) + if err != nil { + return nil, err + } + // Add to the list + responses = append(responses, response) + } + return responses, nil +} diff --git a/cla-backend-go/v2/repositories/handlers.go b/cla-backend-go/v2/repositories/handlers.go index 21d9137e0..f7d97fe36 100644 --- a/cla-backend-go/v2/repositories/handlers.go +++ b/cla-backend-go/v2/repositories/handlers.go @@ -4,11 +4,16 @@ package repositories import ( - "context" "errors" "fmt" "strings" + project_service "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/gitlab_repositories" + + "github.com/communitybridge/easycla/cla-backend-go/github/branch_protection" + "github.com/sirupsen/logrus" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -17,40 +22,47 @@ import ( "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/github_repositories" - "github.com/communitybridge/easycla/cla-backend-go/repositories" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/go-openapi/runtime/middleware" "github.com/jinzhu/copier" ) // Configure establishes the middleware handlers for the repository service -func Configure(api *operations.EasyclaAPI, service Service, eventService events.Service) { +func Configure(api *operations.EasyclaAPI, service ServiceInterface, eventService events.Service) { // nolint api.GithubRepositoriesGetProjectGithubRepositoriesHandler = github_repositories.GetProjectGithubRepositoriesHandlerFunc( func(params github_repositories.GetProjectGithubRepositoriesParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint f := logrus.Fields{ - "functionName": "GitHubRepositoriesGetProjectGithubRepositoriesHandler", + "functionName": "v2.repositories.handlers.GitHubRepositoriesGetProjectGithubRepositoriesHandler", 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) { - msg := fmt.Sprintf("user %s does not have access to Get GitHub Repositories with Project scope of %s", - authUser.UserName, params.ProjectSFID) + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return github_repositories.NewGetProjectGithubRepositoriesNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Get GitHub repositories for Project %s with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) log.WithFields(f).Debug(msg) return github_repositories.NewGetProjectGithubRepositoriesForbidden().WithPayload( utils.ErrorResponseForbidden(reqID, msg)) } - result, err := service.ListProjectRepositories(ctx, params.ProjectSFID) + result, err := service.GitHubListProjectRepositories(ctx, params.ProjectSFID) if err != nil { if strings.ContainsAny(err.Error(), "getProjectNotFound") { msg := fmt.Sprintf("repository not found for projectSFID: %s", params.ProjectSFID) @@ -65,7 +77,7 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - response := &models.ListGithubRepositories{} + response := &models.GithubListRepositories{} err = copier.Copy(response, result) if err != nil { msg := fmt.Sprintf("problem converting response for projectSFID: %s", params.ProjectSFID) @@ -81,48 +93,79 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. func(params github_repositories.AddProjectGithubRepositoryParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint f := logrus.Fields{ - "functionName": "GitHubRepositoriesAddProjectGithubRepositoryHandler", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "authUser": authUser.UserName, - "authEmail": authUser.Email, - "projectSFID": params.ProjectSFID, + "functionName": "v2.repositories.handlers.GitHubRepositoriesAddProjectGithubRepositoryHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + "claGroupID": utils.StringValue(params.GithubRepositoryInput.ClaGroupID), + "githubOrganizationName": utils.StringValue(params.GithubRepositoryInput.GithubOrganizationName), + "repositoryGitHubID": params.GithubRepositoryInput.RepositoryGithubID, + "repositoryGitHubIDs": strings.Join(params.GithubRepositoryInput.RepositoryGithubIds, ","), } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - msg := fmt.Sprintf("user %s does not have access to Add GitHub Repositories with Project scope of %s", - authUser.UserName, params.ProjectSFID) + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return github_repositories.NewAddProjectGithubRepositoryNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to add GitHub repositories for Project %s with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) log.WithFields(f).Debug(msg) return github_repositories.NewAddProjectGithubRepositoryForbidden().WithPayload( utils.ErrorResponseForbidden(reqID, msg)) } - result, err := service.AddGithubRepository(ctx, params.ProjectSFID, params.GithubRepositoryInput) + // If no repository GitHub ID values provided... + // RepositoryGithubID - provided by the older retool UI which provides only one value + // RepositoryGithubIds - provided by new PCC which passes multiple values + if params.GithubRepositoryInput.RepositoryGithubID == "" && len(params.GithubRepositoryInput.RepositoryGithubIds) == 0 { + msg := "missing repository GitHub ID value(s)" + log.WithFields(f).Warn(msg) + return github_repositories.NewAddProjectGithubRepositoryBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + log.WithFields(f).Debugf("Adding GitHub repositories for project: %s", params.ProjectSFID) + results, err := service.GitHubAddRepositories(ctx, params.ProjectSFID, params.GithubRepositoryInput) if err != nil { + if _, ok := err.(*utils.GitHubRepositoryExists); ok { + msg := fmt.Sprintf("unable to add repository - repository already exists for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(err).Warn(msg) + return github_repositories.NewAddProjectGithubRepositoryConflict().WithPayload( + utils.ErrorResponseConflictWithError(reqID, msg, err)) + } msg := fmt.Sprintf("problem adding github repositories for projectSFID: %s", params.ProjectSFID) log.WithFields(f).WithError(err).Warn(msg) return github_repositories.NewAddProjectGithubRepositoryBadRequest().WithPayload( utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - // Log the event - eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryAdded, - ProjectID: utils.StringValue(params.GithubRepositoryInput.ClaGroupID), - ExternalProjectID: params.ProjectSFID, - LfUsername: authUser.UserName, - ClaGroupModel: &v1Models.ClaGroup{ - ProjectExternalID: params.ProjectSFID, - ProjectID: utils.StringValue(params.GithubRepositoryInput.ClaGroupID), - }, - EventData: &events.RepositoryAddedEventData{ - RepositoryName: result.RepositoryName, - }, - }) + // Log the events + for _, result := range results { + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryAdded, + ProjectID: utils.StringValue(params.GithubRepositoryInput.ClaGroupID), + ProjectSFID: params.ProjectSFID, + LfUsername: authUser.UserName, + ClaGroupModel: &v1Models.ClaGroup{ + ProjectExternalID: params.ProjectSFID, + ProjectID: utils.StringValue(params.GithubRepositoryInput.ClaGroupID), + }, + EventData: &events.RepositoryAddedEventData{ + RepositoryName: result.RepositoryName, + }, + }) + } - response := &models.GithubRepository{} - err = copier.Copy(response, result) + var v2ResponseList []*models.GithubRepository + err = copier.Copy(&v2ResponseList, results) if err != nil { msg := fmt.Sprintf("problem converting response for projectSFID: %s", params.ProjectSFID) log.WithFields(f).WithError(err).Warn(msg) @@ -130,16 +173,19 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) } - return github_repositories.NewAddProjectGithubRepositoryOK().WithPayload(response) + v2Response := &models.GithubListRepositories{} + v2Response.List = v2ResponseList + + return github_repositories.NewAddProjectGithubRepositoryOK().WithPayload(v2Response) }) api.GithubRepositoriesDeleteProjectGithubRepositoryHandler = github_repositories.DeleteProjectGithubRepositoryHandlerFunc( func(params github_repositories.DeleteProjectGithubRepositoryParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint f := logrus.Fields{ - "functionName": "GitHubRepositoriesDeleteProjectGithubRepositoryHandler", + "functionName": "v2.repositories.handlers.GitHubRepositoriesDeleteProjectGithubRepositoryHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUser": authUser.UserName, "authEmail": authUser.Email, @@ -147,17 +193,25 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. "repositoryID": params.RepositoryID, } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - msg := fmt.Sprintf("user %s does not have access to Delete GitHub Repositories with Project scope of %s", - authUser.UserName, params.ProjectSFID) + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return github_repositories.NewDeleteProjectGithubRepositoryNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Get GitHub repositories for Project %s with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) log.WithFields(f).Debug(msg) return github_repositories.NewDeleteProjectGithubRepositoryForbidden().WithPayload( utils.ErrorResponseForbidden(reqID, msg)) } - ghRepo, err := service.GetRepository(ctx, params.RepositoryID) + ghRepo, err := service.GitHubGetRepository(ctx, params.RepositoryID) if err != nil { - if err == repositories.ErrGithubRepositoryNotFound { + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { msg := fmt.Sprintf("repository not found for projectSFID: %s", params.ProjectSFID) log.WithFields(f).WithError(err).Warn(msg) return github_repositories.NewDeleteProjectGithubRepositoryNotFound().WithPayload( @@ -170,7 +224,7 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - err = service.DisableRepository(ctx, params.RepositoryID) + err = service.GitHubDisableRepository(ctx, params.RepositoryID) if err != nil { msg := fmt.Sprintf("problem disabling repository for projectSFID: %s, error: %+v", params.ProjectSFID, err) log.WithFields(f).WithError(err).Warn(msg) @@ -178,11 +232,11 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.ErrorResponseBadRequestWithError(reqID, msg, err)) } - eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryDisabled, - ExternalProjectID: params.ProjectSFID, - ProjectID: ghRepo.RepositoryProjectID, - LfUsername: authUser.UserName, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryDisabled, + ProjectSFID: params.ProjectSFID, + CLAGroupID: ghRepo.RepositoryClaGroupID, + LfUsername: authUser.UserName, EventData: &events.RepositoryDisabledEventData{ RepositoryName: ghRepo.RepositoryName, }, @@ -195,9 +249,9 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. func(params github_repositories.GetProjectGithubRepositoryBranchProtectionParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint f := logrus.Fields{ - "functionName": "GitHubRepositoriesGetProjectGithubRepositoryBranchProtectionHandler", + "functionName": "v2.repositories.handlers.GitHubRepositoriesGetProjectGithubRepositoryBranchProtectionHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUser": authUser.UserName, "authEmail": authUser.Email, @@ -205,17 +259,32 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. "repositoryID": params.RepositoryID, } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - msg := fmt.Sprintf("user %s does not have access to Query Protected Branch GitHub Repositories with Project scope of %s", - authUser.UserName, params.ProjectSFID) + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return github_repositories.NewGetProjectGithubRepositoryBranchProtectionNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Query Protected Branch GitHub Repositories for Project %s with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) log.WithFields(f).Debug(msg) return github_repositories.NewGetProjectGithubRepositoryBranchProtectionForbidden().WithPayload( utils.ErrorResponseForbidden(reqID, msg)) } - protectedBranch, err := service.GetProtectedBranch(ctx, params.ProjectSFID, params.RepositoryID) + var branchName string + if params.BranchName == nil || *params.BranchName == "" { + branchName = branch_protection.DefaultBranchName + } else { + branchName = *params.BranchName + } + + protectedBranch, err := service.GitHubGetProtectedBranch(ctx, params.ProjectSFID, params.RepositoryID, branchName) if err != nil { - if err == repositories.ErrGithubRepositoryNotFound { + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { msg := fmt.Sprintf("unable to locatate branch protection projectSFID: %s, repository: %s", params.ProjectSFID, params.RepositoryID) log.WithFields(f).WithError(err).Warn(msg) return github_repositories.NewGetProjectGithubRepositoryBranchProtectionNotFound().WithPayload( @@ -249,9 +318,9 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. func(params github_repositories.UpdateProjectGithubRepositoryBranchProtectionParams, authUser *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) - ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint f := logrus.Fields{ - "functionName": "GitHubRepositoriesUpdateProjectGitHubRepositoryBranchProtectionHandler", + "functionName": "v2.repositories.handlers.GitHubRepositoriesUpdateProjectGitHubRepositoryBranchProtectionHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "authUser": authUser.UserName, "authEmail": authUser.Email, @@ -259,19 +328,27 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. "repositoryID": params.RepositoryID, } - if !utils.IsUserAuthorizedForProjectTree(authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { - msg := fmt.Sprintf("user %s does not have access to Update Protected Branch GitHub Repositories with Project scope of %s", - authUser.UserName, params.ProjectSFID) + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return github_repositories.NewUpdateProjectGithubRepositoryBranchProtectionNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Update Protected Branch GitHub Repositories for Project %s with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) log.WithFields(f).Debug(msg) return github_repositories.NewUpdateProjectGithubRepositoryBranchProtectionForbidden().WithPayload( utils.ErrorResponseForbidden(reqID, msg)) } - protectedBranch, err := service.UpdateProtectedBranch(ctx, params.RepositoryID, params.ProjectSFID, params.GithubRepositoryBranchProtectionInput) + protectedBranch, err := service.GitHubUpdateProtectedBranch(ctx, params.ProjectSFID, params.RepositoryID, params.GithubRepositoryBranchProtectionInput) if err != nil { - log.Warnf("update protected branch failed for repo %s : %v", params.RepositoryID, err) - if err == repositories.ErrGithubRepositoryNotFound { - msg := fmt.Sprintf("unable to update branch protection projectSFID: %s, repository: %s", params.ProjectSFID, params.RepositoryID) + log.Warnf("update protected branch failed for gitV1Repository %s : %v", params.RepositoryID, err) + if _, ok := err.(*utils.GitHubRepositoryNotFound); ok { + msg := fmt.Sprintf("unable to update branch protection for projectSFID: %s, repository: %s", params.ProjectSFID, params.RepositoryID) log.WithFields(f).WithError(err).Warn(msg) return github_repositories.NewGetProjectGithubRepositoryBranchProtectionNotFound().WithPayload( utils.ErrorResponseNotFound(reqID, msg)) @@ -304,7 +381,7 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) } - repoModel, repoErr := service.GetRepository(ctx, params.RepositoryID) + repoModel, repoErr := service.GitHubGetRepository(ctx, params.RepositoryID) if repoErr != nil { msg := fmt.Sprintf("problem fetching the repository for projectSFID: %s, with repository: %s, error: %+v", params.ProjectSFID, params.RepositoryID, err) log.WithFields(f).WithError(repoErr).Warning(msg) @@ -314,11 +391,11 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. // We could extract the parameter values from the branch protection payload to determine if it was added/remove or simply updated // For now, let's just set the updated event log - eventService.LogEvent(&events.LogEventArgs{ - EventType: events.RepositoryBranchProtectionUpdated, - ExternalProjectID: params.ProjectSFID, - ProjectID: params.ProjectSFID, - LfUsername: authUser.UserName, + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryBranchProtectionUpdated, + ProjectSFID: params.ProjectSFID, + ProjectID: params.ProjectSFID, + LfUsername: authUser.UserName, EventData: &events.RepositoryBranchProtectionUpdatedEventData{ RepositoryName: repoModel.RepositoryName, }, @@ -326,4 +403,162 @@ func Configure(api *operations.EasyclaAPI, service Service, eventService events. return github_repositories.NewGetProjectGithubRepositoryBranchProtectionOK().WithPayload(protectedBranch) }) + + api.GitlabRepositoriesGetProjectGitLabRepositoriesHandler = gitlab_repositories.GetProjectGitLabRepositoriesHandlerFunc( + func(params gitlab_repositories.GetProjectGitLabRepositoriesParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + f := logrus.Fields{ + "functionName": "v2.repositories.handlers.GitlabRepositoriesGetProjectGitLabRepositoriesHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + } + + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return gitlab_repositories.NewGetProjectGitLabRepositoriesNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Get GitLab Repositories for Project %s with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_repositories.NewGetProjectGitLabRepositoriesForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + + result, err := service.GitLabGetRepositoriesByProjectSFID(ctx, params.ProjectSFID) + if err != nil { + if strings.ContainsAny(err.Error(), "getProjectNotFound") { + msg := fmt.Sprintf("repository not found for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(err).Warn(msg) + return gitlab_repositories.NewGetProjectGitLabRepositoriesNotFound().WithXRequestID(reqID).WithPayload(utils.ErrorResponseNotFound(reqID, msg)) + } + + msg := fmt.Sprintf("problem looking up repositories for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(err).Warn(msg) + return gitlab_repositories.NewGetProjectGitLabRepositoriesBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + response := &models.GitlabRepositoriesList{} + err = copier.Copy(response, result) + if err != nil { + msg := fmt.Sprintf("problem converting response for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(err).Warn(msg) + return gitlab_repositories.NewGetProjectGitLabRepositoriesInternalServerError().WithXRequestID(reqID).WithPayload(utils.ErrorResponseInternalServerErrorWithError(reqID, msg, err)) + } + + return gitlab_repositories.NewGetProjectGitLabRepositoriesOK().WithPayload(response) + }) + + api.GitlabRepositoriesEnrollGitLabRepositoryHandler = gitlab_repositories.EnrollGitLabRepositoryHandlerFunc( + func(params gitlab_repositories.EnrollGitLabRepositoryParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, authUser) // nolint + f := logrus.Fields{ + "functionName": "v2.repositories.handlers.GitlabRepositoriesEnableGitLabRepositoryHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + "claGroupID": params.GitlabRepositoriesEnroll.ClaGroupID, + } + + // Load the project + psc := project_service.GetClient() + projectModel, err := psc.GetProject(params.ProjectSFID) + if err != nil || projectModel == nil { + return gitlab_repositories.NewEnrollGitLabRepositoryNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, fmt.Sprintf("unable to locate project with ID: %s", params.ProjectSFID))) + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Add GitLab Repositories for Project '%s' with scope of %s", + authUser.UserName, projectModel.Name, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_repositories.NewEnrollGitLabRepositoryForbidden().WithXRequestID(reqID).WithPayload(utils.ErrorResponseForbidden(reqID, msg)) + } + + if len(params.GitlabRepositoriesEnroll.Enroll) == 0 && len(params.GitlabRepositoriesEnroll.Unenroll) == 0 { + msg := "missing GitLab ID values for enroll or unenroll - should have at least one value in either list" + return gitlab_repositories.NewEnrollGitLabRepositoryBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + } + + if duplicates := utils.FindInt64Duplicates(params.GitlabRepositoriesEnroll.Enroll, params.GitlabRepositoriesEnroll.Unenroll); len(duplicates) > 0 { + msg := fmt.Sprintf("found duplicate entries in both enroll and unenroll: %+v", duplicates) + return gitlab_repositories.NewEnrollGitLabRepositoryBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequest(reqID, msg)) + } + + if len(params.GitlabRepositoriesEnroll.Enroll) > 0 { + log.WithFields(f).Debugf("enrolling GitLab repository for project: %s and CLA Group: %s", params.ProjectSFID, params.GitlabRepositoriesEnroll.ClaGroupID) + enableErr := service.GitLabEnrollRepositories(ctx, params.GitlabRepositoriesEnroll.ClaGroupID, params.GitlabRepositoriesEnroll.Enroll, true) + if enableErr != nil { + msg := fmt.Sprintf("problem enrolling GitLab repositories for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(enableErr).Warn(msg) + return gitlab_repositories.NewEnrollGitLabRepositoryBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, enableErr)) + } + + // Log unenroll gitlab project event + for _, externalID := range params.GitlabRepositoriesEnroll.Enroll { + gitlabRepo, err := service.GitLabGetRepositoryByExternalID(ctx, externalID) + if err != nil { + log.WithFields(f).Errorf("unable to fetch repository by externalID: %d: error: %+v", externalID, err) + continue + } + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryAdded, + ProjectSFID: params.ProjectSFID, + CLAGroupID: gitlabRepo.RepositoryClaGroupID, + LfUsername: authUser.UserName, + EventData: &events.RepositoryAddedEventData{ + RepositoryName: gitlabRepo.RepositoryName, + RepositoryType: utils.GitLabRepositoryType, + }, + }) + } + } + + if len(params.GitlabRepositoriesEnroll.Unenroll) > 0 { + log.WithFields(f).Debugf("unenrolling GitLab repository for project: %s and CLA Group: %s", params.ProjectSFID, params.GitlabRepositoriesEnroll.ClaGroupID) + enableErr := service.GitLabEnrollRepositories(ctx, params.GitlabRepositoriesEnroll.ClaGroupID, params.GitlabRepositoriesEnroll.Unenroll, false) + if enableErr != nil { + msg := fmt.Sprintf("problem unenrolling GitLab repositories for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(enableErr).Warn(msg) + return gitlab_repositories.NewEnrollGitLabRepositoryBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, enableErr)) + } + // Log unenroll gitlab project event + for _, externalID := range params.GitlabRepositoriesEnroll.Unenroll { + gitlabRepo, err := service.GitLabGetRepositoryByExternalID(ctx, externalID) + if err != nil { + log.WithFields(f).Errorf("unable to fetch repository by externalID: %d: error: %+v", externalID, err) + continue + } + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + EventType: events.RepositoryDisabled, + ProjectSFID: params.ProjectSFID, + CLAGroupID: gitlabRepo.RepositoryClaGroupID, + LfUsername: authUser.UserName, + EventData: &events.RepositoryDisabledEventData{ + RepositoryName: gitlabRepo.RepositoryName, + RepositoryType: utils.GitLabRepositoryType, + }, + }) + } + } + + repoList, getErr := service.GitLabGetRepositoriesByProjectSFID(ctx, params.ProjectSFID) + if getErr != nil { + msg := fmt.Sprintf("problem fetching GitLab repositories for projectSFID: %s", params.ProjectSFID) + log.WithFields(f).WithError(getErr).Warn(msg) + return gitlab_repositories.NewEnrollGitLabRepositoryBadRequest().WithXRequestID(reqID).WithPayload(utils.ErrorResponseBadRequestWithError(reqID, msg, getErr)) + } + + return gitlab_repositories.NewEnrollGitLabRepositoryOK().WithPayload(repoList) + }) } diff --git a/cla-backend-go/v2/repositories/models.go b/cla-backend-go/v2/repositories/models.go new file mode 100644 index 000000000..d25c6a5c0 --- /dev/null +++ b/cla-backend-go/v2/repositories/models.go @@ -0,0 +1,13 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package repositories + +// GitLabAddRepoModel data model for GitLab add repository +type GitLabAddRepoModel struct { + ClaGroupID string + GroupName string + ExternalID int64 + GroupFullPath string + ProjectIDList []int64 +} diff --git a/cla-backend-go/v2/repositories/repository.go b/cla-backend-go/v2/repositories/repository.go new file mode 100644 index 000000000..4ae3f7c6b --- /dev/null +++ b/cla-backend-go/v2/repositories/repository.go @@ -0,0 +1,670 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package repositories + +import ( + "context" + "fmt" + "strconv" + "strings" + + "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" + repoModels "github.com/communitybridge/easycla/cla-backend-go/repositories" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/gofrs/uuid" + "github.com/sirupsen/logrus" +) + +// RepositoryInterface interface defines the functions for the GitLab repository data model +type RepositoryInterface interface { + GitHubGetRepositoriesByCLAGroup(ctx context.Context, claGroupID string) ([]*repoModels.RepositoryDBModel, error) + GitHubGetRepositoriesByCLAGroupEnabled(ctx context.Context, claGroupID string) ([]*repoModels.RepositoryDBModel, error) + GitHubGetRepositoriesByCLAGroupDisabled(ctx context.Context, claGroupID string) ([]*repoModels.RepositoryDBModel, error) + GitHubGetRepositoriesByProjectSFID(ctx context.Context, projectSFID string) ([]*repoModels.RepositoryDBModel, error) + GitHubGetRepositoriesByOrganizationName(ctx context.Context, orgName string) ([]*repoModels.RepositoryDBModel, error) + + GitLabGetRepository(ctx context.Context, repositoryID string) (*repoModels.RepositoryDBModel, error) + GitLabGetRepositoryByName(ctx context.Context, repositoryName string) (*repoModels.RepositoryDBModel, error) + GitLabGetRepositoriesByOrganizationName(ctx context.Context, orgName string) ([]*repoModels.RepositoryDBModel, error) + GitLabGetRepositoriesByNamePrefix(ctx context.Context, repositoryNamePrefix string) ([]*repoModels.RepositoryDBModel, error) + GitLabGetRepositoryByExternalID(ctx context.Context, repositoryExternalID int64) (*repoModels.RepositoryDBModel, error) + GitLabAddRepository(ctx context.Context, projectSFID string, input *repoModels.RepositoryDBModel) (*repoModels.RepositoryDBModel, error) + GitLabEnrollRepositoryByID(ctx context.Context, claGroupID string, repositoryID int64, enrollValue bool) error + GitLabEnableCLAGroupRepositories(ctx context.Context, claGroupID string, enrollValue bool) error + GitLabDeleteRepositories(ctx context.Context, gitLabGroupPath string) error + GitLabDeleteRepositoryByExternalID(ctx context.Context, gitLabExternalID int64) error +} + +// Repository object/struct +type Repository struct { + stage string + dynamoDBClient *dynamodb.DynamoDB + repositoryTableName string + gitLabOrgTableName string +} + +// NewRepository creates a new instance of the GitLab repository service +func NewRepository(awsSession *session.Session, stage string) *Repository { + return &Repository{ + stage: stage, + dynamoDBClient: dynamodb.New(awsSession), + repositoryTableName: fmt.Sprintf("cla-%s-repositories", stage), + gitLabOrgTableName: fmt.Sprintf("cla-%s-gitlab-orgs", stage), + } +} + +// GitLabGetRepository returns the database model for the internal repository ID +func (r *Repository) GitLabGetRepository(ctx context.Context, repositoryID string) (*repoModels.RepositoryDBModel, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.repositories.GitLabGetRepository", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "repositoryID": repositoryID, + } + + result, err := r.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ + TableName: aws.String(r.repositoryTableName), + Key: map[string]*dynamodb.AttributeValue{ + "repository_id": { + S: aws.String(repositoryID), + }, + }, + }) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem querying using repository ID") + return nil, err + } + if len(result.Item) == 0 { + msg := fmt.Sprintf("repository with ID: %s does not exist", repositoryID) + log.WithFields(f).Warn(msg) + return nil, &utils.GitHubRepositoryNotFound{ + Message: msg, + } + } + + // Decode the results into a model + var out repoModels.RepositoryDBModel + err = dynamodbattribute.UnmarshalMap(result.Item, &out) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling database repository response") + return nil, err + } + + return &out, nil +} + +// GitLabGetRepositoryByName returns the database model for the specified repository +func (r *Repository) GitLabGetRepositoryByName(ctx context.Context, repositoryName string) (*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryNameColumn).Equal(expression.Value(repositoryName)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)) + record, err := r.getRepositoryWithConditionFilter(ctx, condition, filter, repoModels.RepositoryNameIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + RepositoryName: repositoryName, + } + } + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabDuplicateRepositoriesFound); ok { + return nil, &utils.GitLabDuplicateRepositoriesFound{ + RepositoryName: repositoryName, + } + } + // Some other error + return nil, err + } + + return record, nil +} + +// GitLabGetRepositoriesByNamePrefix returns a list of repositories matching the specified name prefix +func (r *Repository) GitLabGetRepositoriesByNamePrefix(ctx context.Context, repositoryNamePrefix string) ([]*repoModels.RepositoryDBModel, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.repositories.GitLabGetRepositoriesByNamePrefix", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "repositoryNamePrefix": repositoryNamePrefix, + } + + log.WithFields(f).Debug("querying for repositories with name prefix") + condition := expression.Key(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)) + filter := expression.Name(repoModels.RepositoryNameColumn).BeginsWith(repositoryNamePrefix) + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryTypeIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + RepositoryName: repositoryNamePrefix, + } + } + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabDuplicateRepositoriesFound); ok { + return nil, &utils.GitLabDuplicateRepositoriesFound{ + RepositoryName: repositoryNamePrefix, + } + } + // Some other error + return nil, err + } + + return records, nil +} + +// GitLabGetRepositoryByExternalID returns the database model for the specified repository by external ID +func (r *Repository) GitLabGetRepositoryByExternalID(ctx context.Context, repositoryExternalID int64) (*repoModels.RepositoryDBModel, error) { + str := strconv.FormatInt(repositoryExternalID, 10) + condition := expression.Key(repoModels.RepositoryExternalIDColumn).Equal(expression.Value(str)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)) + record, err := r.getRepositoryWithConditionFilter(ctx, condition, filter, repoModels.RepositoryExternalIDIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + RepositoryExternalID: repositoryExternalID, + } + } + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabDuplicateRepositoriesFound); ok { + return nil, &utils.GitLabDuplicateRepositoriesFound{ + RepositoryExternalID: repositoryExternalID, + } + } + // Some other error + return nil, err + } + + return record, nil +} + +// GitHubGetRepositoriesByCLAGroup returns the database models for the specified CLA Group ID +func (r *Repository) GitHubGetRepositoriesByCLAGroup(ctx context.Context, claGroupID string) ([]*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryCLAGroupIDColumn).Equal(expression.Value(claGroupID)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)) + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryProjectIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + CLAGroupID: claGroupID, + } + } + + // Some other error + return nil, err + } + + return records, nil +} + +// GitHubGetRepositoriesByCLAGroupEnabled returns the database models for the specified CLA Group ID that are enabled +func (r *Repository) GitHubGetRepositoriesByCLAGroupEnabled(ctx context.Context, claGroupID string) ([]*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryCLAGroupIDColumn).Equal(expression.Value(claGroupID)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)). + And(expression.Name(repoModels.RepositoryEnabledColumn).Equal(expression.Value(true))) + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryProjectIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + CLAGroupID: claGroupID, + } + } + + // Some other error + return nil, err + } + + return records, nil +} + +// GitHubGetRepositoriesByCLAGroupDisabled returns the database models for the specified CLA Group ID that are disabled +func (r *Repository) GitHubGetRepositoriesByCLAGroupDisabled(ctx context.Context, claGroupID string) ([]*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryCLAGroupIDColumn).Equal(expression.Value(claGroupID)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)). + And(expression.Name(repoModels.RepositoryEnabledColumn).Equal(expression.Value(false))) + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryProjectIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + CLAGroupID: claGroupID, + } + } + + // Some other error + return nil, err + } + + return records, nil +} + +// GitHubGetRepositoriesByProjectSFID returns a list of repositories associated with the specified project +func (r *Repository) GitHubGetRepositoriesByProjectSFID(ctx context.Context, projectSFID string) ([]*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryProjectIDColumn).Equal(expression.Value(projectSFID)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)) + + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryProjectSFIDIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + ProjectSFID: projectSFID, + } + } + + // Some other error + return nil, err + } + + return records, nil +} + +// GitHubGetRepositoriesByOrganizationName returns a list of GitHub repositories associated with the specified organization name +func (r *Repository) GitHubGetRepositoriesByOrganizationName(ctx context.Context, orgName string) ([]*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryOrganizationNameColumn).Equal(expression.Value(orgName)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitHubType)) + + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryOrganizationNameIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + OrganizationName: orgName, + } + } + + // Some other error + return nil, err + } + + return records, nil +} + +// GitLabGetRepositoriesByOrganizationName returns a list of GitLab repositories associated with the specified organization name +func (r *Repository) GitLabGetRepositoriesByOrganizationName(ctx context.Context, orgName string) ([]*repoModels.RepositoryDBModel, error) { + condition := expression.Key(repoModels.RepositoryOrganizationNameColumn).Equal(expression.Value(orgName)) + filter := expression.Name(repoModels.RepositoryTypeColumn).Equal(expression.Value(utils.GitLabLower)) + + records, err := r.getRepositoriesWithConditionFilter(ctx, condition, filter, repoModels.RepositoryOrganizationNameIndex) + if err != nil { + // Catch the error - return the same error with the appropriate details + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil, &utils.GitLabRepositoryNotFound{ + OrganizationName: orgName, + } + } + + // Some other error + return nil, err + } + + return records, nil +} + +// GitLabAddRepository creates a new entry in the repositories table using the specified input parameters +func (r *Repository) GitLabAddRepository(ctx context.Context, projectSFID string, input *repoModels.RepositoryDBModel) (*repoModels.RepositoryDBModel, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.repositories.GitHubAddRepositories", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "repositoryExternalID": input.RepositoryExternalID, + "repositoryURL": input.RepositoryURL, + "repositoryName": input.RepositoryName, + "repositoryFullPath": input.RepositoryFullPath, + "repositoryType": utils.GitLabLower, + "repositoryCLAGroupID": input.RepositoryCLAGroupID, + "repositoryProjectSFID": input.RepositorySfdcID, + "repositoryOrganizationName": input.RepositoryOrganizationName, + } + + // Check first to see if the repository already exists + _, err := r.GitLabGetRepositoryByName(ctx, input.RepositoryName) + if err != nil { + // Expecting Not found - no issue if not found - all other error we throw + if _, ok := err.(*utils.GitLabRepositoryNotFound); !ok { + return nil, err + } + } else { + return nil, &utils.GitLabRepositoryExists{ + Message: fmt.Sprintf("GitLab repository with name: %s has already been registered", input.RepositoryName), + RepositoryName: "", + Err: nil, + } + } + + _, currentTime := utils.CurrentTime() + repoID, err := uuid.NewV4() + if err != nil { + return nil, err + } + + input.RepositoryID = repoID.String() + input.DateCreated = currentTime + input.DateModified = currentTime + input.Note = fmt.Sprintf("created on %s", currentTime) + input.Version = "v1" + + av, err := dynamodbattribute.MarshalMap(input) + if err != nil { + log.WithFields(f).Warnf("problem marshalling the input, error: %+v", err) + return nil, err + } + + _, err = r.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(r.repositoryTableName), + }) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to add github repository") + return nil, err + } + + return input, nil +} + +// GitLabEnrollRepositoryByID enables the specified repository +func (r *Repository) GitLabEnrollRepositoryByID(ctx context.Context, claGroupID string, repositoryExternalID int64, enrollValue bool) error { + return r.setRepositoryEnabledValue(ctx, claGroupID, repositoryExternalID, enrollValue) +} + +// GitLabEnableCLAGroupRepositories enables the specified CLA Group repositories +func (r *Repository) GitLabEnableCLAGroupRepositories(ctx context.Context, claGroupID string, enrollValue bool) error { + repositories, err := r.GitHubGetRepositoriesByCLAGroup(ctx, claGroupID) + if err != nil { + return err + } + + for _, repo := range repositories { + int64I, parseErr := strconv.ParseInt(repo.RepositoryExternalID, 10, 64) + if parseErr != nil { + return parseErr + } + + enableErr := r.GitLabEnrollRepositoryByID(ctx, claGroupID, int64I, enrollValue) + if enableErr != nil { + return enableErr + } + } + + return nil +} + +// GitLabDeleteRepositories deletes the specified repositories under the GitLap group path +func (r *Repository) GitLabDeleteRepositories(ctx context.Context, gitLabGroupPath string) error { + f := logrus.Fields{ + "functionName": "v2.repositories.repository.GitLabDeleteRepositories", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabGroupPath": gitLabGroupPath, + } + + log.WithFields(f).Debugf("loading repositories with name prefix: %s", gitLabGroupPath) + repositories, err := r.GitLabGetRepositoriesByNamePrefix(ctx, gitLabGroupPath) + if err != nil { + // If nothing to delete... + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil + } + log.WithFields(f).WithError(err).Warnf("problem loading repositories with name prefix: %s", gitLabGroupPath) + return err + } + log.WithFields(f).Debugf("processing repository delete request for %d repositories", len(repositories)) + + type GitLabDeleteRepositoryResponse struct { + RepositoryID string + RepositoryName string + RepositoryFullPath string + Error error + } + deleteRepoRespChan := make(chan *GitLabDeleteRepositoryResponse, len(repositories)) + + for _, repo := range repositories { + go func(repo *repoModels.RepositoryDBModel) { + _, err = r.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + repoModels.RepositoryIDColumn: {S: aws.String(repo.RepositoryID)}, + }, + TableName: aws.String(r.repositoryTableName), + }) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error deleting repository with ID:%s", repo.RepositoryID) + } + deleteRepoRespChan <- &GitLabDeleteRepositoryResponse{ + RepositoryID: repo.RepositoryID, + RepositoryName: repo.RepositoryName, + RepositoryFullPath: repo.RepositoryFullPath, + Error: err, + } + }(repo) + } + + // Wait for the go routines to finish and load up the results + log.WithFields(f).Debug("waiting for delete repos to finish...") + var lastErr error + for range repositories { + select { + case response := <-deleteRepoRespChan: + if response.Error != nil { + log.WithFields(f).WithError(response.Error).Warn(response.Error.Error()) + lastErr = response.Error + } else { + log.WithFields(f).Debugf("delete repo: %s with ID: %s with full path: %s", response.RepositoryName, response.RepositoryID, response.RepositoryFullPath) + } + case <-ctx.Done(): + log.WithFields(f).WithError(ctx.Err()).Warnf("waiting for delete repositories timed out") + lastErr = fmt.Errorf("delete repositories failed with timeout, error: %v", ctx.Err()) + } + } + + // Return the last error, hopefully nil if no error occurred... + return lastErr +} + +// GitLabDeleteRepositoryByExternalID deletes the specified repository +func (r *Repository) GitLabDeleteRepositoryByExternalID(ctx context.Context, gitLabExternalID int64) error { + f := logrus.Fields{ + "functionName": "v2.repositories.repository.GitLabDeleteRepositoryByExternalID", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitLabExternalID": gitLabExternalID, + } + + repositoryRecord, err := r.GitLabGetRepositoryByExternalID(ctx, gitLabExternalID) + if err != nil { + // If nothing to delete... + if _, ok := err.(*utils.GitLabRepositoryNotFound); ok { + return nil + } + log.WithFields(f).WithError(err).Warnf("problem loading existing repository by external ID: %d", gitLabExternalID) + return err + } + if repositoryRecord == nil { + return nil + } + + _, deleteErr := r.dynamoDBClient.DeleteItem(&dynamodb.DeleteItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + repoModels.RepositoryIDColumn: {S: aws.String(repositoryRecord.RepositoryID)}, + }, + TableName: aws.String(r.repositoryTableName), + }) + + // Return the error + return deleteErr +} + +// getRepositoryWithConditionFilter fetches the repository entry based on the specified condition and filter criteria using the provided index +func (r *Repository) getRepositoryWithConditionFilter(ctx context.Context, condition expression.KeyConditionBuilder, filter expression.ConditionBuilder, indexName string) (*repoModels.RepositoryDBModel, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.repository.getRepositoryWithConditionFilter", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "indexName": indexName, + } + + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter).Build() + if err != nil { + log.WithFields(f).WithError(err).Warn("problem creating builder") + return nil, err + } + + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(r.repositoryTableName), + IndexName: aws.String(indexName), + } + + results, err := r.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get repositories using query: %+v", queryInput) + return nil, err + } + + if len(results.Items) == 0 { + // Generic - no details as we don't know what filter content was provided + return nil, &utils.GitLabRepositoryNotFound{} + } + + var repositories []*repoModels.RepositoryDBModel + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &repositories) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling response") + return nil, err + } + + if len(repositories) > 1 { + log.WithFields(f).Warn("multiple repositories records with the same repository name and type found") + // Generic - no details as we don't know what filter content was provided + return nil, &utils.GitLabDuplicateRepositoriesFound{} + } + + return repositories[0], nil +} + +// getRepositoriesWithConditionFilter fetches the repository entry based on the specified condition and filter criteria +// using the provided index +func (r *Repository) getRepositoriesWithConditionFilter(ctx context.Context, condition expression.KeyConditionBuilder, filter expression.ConditionBuilder, indexName string) ([]*repoModels.RepositoryDBModel, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.repository.getRepositoriesWithConditionFilter", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "indexName": indexName, + } + + expr, err := expression.NewBuilder().WithKeyCondition(condition).WithFilter(filter).Build() + if err != nil { + log.WithFields(f).WithError(err).Warn("problem creating builder") + return nil, err + } + + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(r.repositoryTableName), + IndexName: aws.String(indexName), + } + + results, err := r.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get repositories using query: %+v", queryInput) + return nil, err + } + + if len(results.Items) == 0 { + log.WithFields(f).Debugf("no repositories found matching filter critera: %+v", queryInput) + // Generic - no details as we don't know what filter content was provided + return nil, &utils.GitLabRepositoryNotFound{} + } + + var repositories []*repoModels.RepositoryDBModel + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &repositories) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling response") + return nil, err + } + + return repositories, nil +} + +// setRepositoryEnabledValue sets the specified repository to the specified enabled value +func (r *Repository) setRepositoryEnabledValue(ctx context.Context, claGroupID string, repositoryExternalID int64, enabledValue bool) error { + f := logrus.Fields{ + "functionName": "v2.repositories.repository.setRepositoryEnabledValue", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "claGroupID": claGroupID, + "repositoryExternalID": repositoryExternalID, + "enabledValue": enabledValue, + } + + // Load the existing model - need to fetch the old values, if available + existingModel, getErr := r.GitLabGetRepositoryByExternalID(ctx, repositoryExternalID) + if getErr != nil { + return getErr + } + if existingModel == nil { + return fmt.Errorf("unable to locate existing repository entry by external ID: %d", repositoryExternalID) + } + + // If we have an old note - grab it/save it + var existingNote = "" + if existingModel.Note != "" { + if !strings.HasSuffix(strings.TrimSpace(existingModel.Note), ".") { + existingNote = strings.TrimSpace(existingModel.Note) + ". " + } else { + existingNote = strings.TrimSpace(existingModel.Note) + " " + } + } + userNameFromCtx := utils.GetUserNameFromContext(ctx) + byUserStr := "" + if userNameFromCtx != "" { + byUserStr = fmt.Sprintf("by user: %s", userNameFromCtx) + } + + _, now := utils.CurrentTime() + updateInput := &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: map[string]*string{ + "#enabled": aws.String(repoModels.RepositoryEnabledColumn), + "#note": aws.String(repoModels.RepositoryNoteColumn), + "#dateModified": aws.String(repoModels.RepositoryDateModifiedColumn), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":enabledValue": { + BOOL: aws.Bool(enabledValue), + }, + ":noteValue": { + S: aws.String(fmt.Sprintf("%s Updated enabled flag to %t on %s %s.", existingNote, enabledValue, now, byUserStr)), + }, + ":dateModifiedValue": { + S: aws.String(now), + }, + }, + Key: map[string]*dynamodb.AttributeValue{ + repoModels.RepositoryIDColumn: {S: aws.String(existingModel.RepositoryID)}, + }, + TableName: aws.String(r.repositoryTableName), + UpdateExpression: aws.String("SET #enabled = :enabledValue, #note = :noteValue, #dateModified = :dateModifiedValue"), + } + + if claGroupID != "" { + updateInput.ExpressionAttributeNames["#claGroupID"] = aws.String(repoModels.RepositoryCLAGroupIDColumn) + updateInput.ExpressionAttributeValues[":claGroupIDValue"] = &dynamodb.AttributeValue{S: aws.String(claGroupID)} + updateExpression := fmt.Sprintf("%s, #claGroupID = :claGroupIDValue ", *updateInput.UpdateExpression) + updateInput.UpdateExpression = aws.String(updateExpression) + } + + _, err := r.dynamoDBClient.UpdateItem(updateInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem with update, error: %+v", err.Error()) + } + + return err +} diff --git a/cla-backend-go/v2/repositories/service.go b/cla-backend-go/v2/repositories/service.go index cadc50f80..a9dfe6385 100644 --- a/cla-backend-go/v2/repositories/service.go +++ b/cla-backend-go/v2/repositories/service.go @@ -9,19 +9,27 @@ import ( "fmt" "strconv" - "github.com/sirupsen/logrus" + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" - "github.com/go-openapi/swag" - githubsdk "github.com/google/go-github/v33/github" + "github.com/communitybridge/easycla/cla-backend-go/v2/common" + + "github.com/communitybridge/easycla/cla-backend-go/config" + gitLabApi "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" + + "github.com/communitybridge/easycla/cla-backend-go/github/branch_protection" + + "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + "github.com/go-openapi/swag" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/communitybridge/easycla/cla-backend-go/github" "github.com/aws/aws-sdk-go/aws" - v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/models" + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/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/projects_cla_groups" @@ -29,29 +37,62 @@ import ( v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" ) -// Service contains functions of Github Repositories service -type Service interface { - AddGithubRepository(ctx context.Context, projectSFID string, input *models.GithubRepositoryInput) (*v1Models.GithubRepository, error) - EnableRepository(ctx context.Context, repositoryID string) error - DisableRepository(ctx context.Context, repositoryID string) error - ListProjectRepositories(ctx context.Context, projectSFID string) (*v1Models.ListGithubRepositories, error) - GetRepository(ctx context.Context, repositoryID string) (*v1Models.GithubRepository, error) - DisableCLAGroupRepositories(ctx context.Context, claGroupID string) error - GetProtectedBranch(ctx context.Context, projectSFID, repositoryID string) (*v2Models.GithubRepositoryBranchProtection, error) - UpdateProtectedBranch(ctx context.Context, projectSFID, repositoryID string, input *v2Models.GithubRepositoryBranchProtectionInput) (*v2Models.GithubRepositoryBranchProtection, error) +// ServiceInterface contains functions of the repositories service +type ServiceInterface interface { + // GitHub + + GitHubAddRepositories(ctx context.Context, projectSFID string, input *models.GithubRepositoryInput) ([]*v1Models.GithubRepository, error) + GitHubEnableRepository(ctx context.Context, repositoryID string) error + GitHubDisableRepository(ctx context.Context, repositoryID string) error + GitHubListProjectRepositories(ctx context.Context, projectSFID string) (*v1Models.GithubListRepositories, error) + GitHubGetRepository(ctx context.Context, repositoryID string) (*v1Models.GithubRepository, error) + GitHubGetRepositoryByName(ctx context.Context, repositoryName string) (*v1Models.GithubRepository, error) + GitHubGetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*v1Models.GithubRepository, error) + GitHubDisableCLAGroupRepositories(ctx context.Context, claGroupID string) error + GitHubGetProtectedBranch(ctx context.Context, projectSFID, repositoryID, branchName string) (*v2Models.GithubRepositoryBranchProtection, error) + GitHubUpdateProtectedBranch(ctx context.Context, projectSFID, repositoryID string, input *v2Models.GithubRepositoryBranchProtectionInput) (*v2Models.GithubRepositoryBranchProtection, error) + + // GitLab + + GitLabGetRepository(ctx context.Context, repositoryID string) (*v2Models.GitlabRepository, error) + GitLabGetRepositoryByName(ctx context.Context, repositoryName string) (*v2Models.GitlabRepository, error) + GitLabGetRepositoryByExternalID(ctx context.Context, repositoryExternalID int64) (*v2Models.GitlabRepository, error) + GitLabGetRepositoriesByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabRepositoriesList, error) + GitLabGetRepositoriesByCLAGroup(ctx context.Context, claGroupID string, enabled bool) (*v2Models.GitlabRepositoriesList, error) + GitLabGetRepositoriesByOrganizationName(ctx context.Context, orgName string) (*v2Models.GitlabRepositoriesList, error) + GitLabGetRepositoriesByNamePrefix(ctx context.Context, repositoryNamePrefix string) (*v2Models.GitlabRepositoriesList, error) + GitLabAddRepositories(ctx context.Context, projectSFID string, input *GitLabAddRepoModel) (*v2Models.GitlabRepositoriesList, error) + GitLabAddRepositoriesWithEnabledFlag(ctx context.Context, projectSFID string, input *GitLabAddRepoModel, enabled bool) (*v2Models.GitlabRepositoriesList, error) + GitLabAddRepositoriesByApp(ctx context.Context, gitLabOrgModel *common.GitLabOrganization) ([]*v2Models.GitlabRepository, error) + GitLabEnrollRepositories(ctx context.Context, claGroupID string, repositoryIDList []int64, enrollValue bool) error + GitLabEnrollRepository(ctx context.Context, claGroupID string, repositoryExternalID int64, enrollValue bool) error + GitLabEnrollCLAGroupRepositories(ctx context.Context, claGroupID string, enrollValue bool) error + GitLabDeleteRepositories(ctx context.Context, gitLabGroupPath string) error + GitLabDeleteRepositoryByExternalID(ctx context.Context, gitLabExternalID int64) error } -// GithubOrgRepo provide method to get github organization by name -type GithubOrgRepo interface { - GetGithubOrganizationByName(ctx context.Context, githubOrganizationName string) (*v1Models.GithubOrganizations, error) - GetGithubOrganization(ctx context.Context, githubOrganizationName string) (*v1Models.GithubOrganization, error) - GetGithubOrganizations(ctx context.Context, projectSFID string) (*v1Models.GithubOrganizations, error) +// GitLabOrgRepo redefine the interface here to avoid circular dependency issues +type GitLabOrgRepo interface { + AddGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization, enabled bool) (*v2Models.GitlabOrganization, error) + GetGitLabOrganizationsByProjectSFID(ctx context.Context, projectSFID string) (*v2Models.GitlabOrganizations, error) + GetGitLabOrganization(ctx context.Context, gitlabOrganizationID string) (*common.GitLabOrganization, error) + GetGitLabOrganizationByName(ctx context.Context, gitLabOrganizationName string) (*common.GitLabOrganization, error) + GetGitLabOrganizationByExternalID(ctx context.Context, gitLabGroupID int64) (*common.GitLabOrganization, error) + GetGitLabOrganizationByFullPath(ctx context.Context, groupFullPath string) (*common.GitLabOrganization, error) + UpdateGitLabOrganizationAuth(ctx context.Context, organizationID string, gitLabGroupID int, authExpiryTime int64, authInfo, groupName, groupFullPath, organizationURL string) error + UpdateGitLabOrganization(ctx context.Context, input *common.GitLabAddOrganization, enabled bool) error + DeleteGitLabOrganizationByFullPath(ctx context.Context, projectSFID, gitlabOrgFullPath string) error } -type service struct { - repo v1Repositories.Repository +// Service is the service model/structure +type Service struct { + gitV1Repository v1Repositories.RepositoryInterface + gitV2Repository RepositoryInterface projectsClaGroupsRepo projects_cla_groups.Repository - ghOrgRepo GithubOrgRepo + ghOrgRepo github_organizations.RepositoryInterface + glOrgRepo GitLabOrgRepo + gitLabApp *gitLabApi.App + eventService events.Service } var ( @@ -61,36 +102,46 @@ var ( ) // NewService creates a new githubOrganizations service -func NewService(repo v1Repositories.Repository, pcgRepo projects_cla_groups.Repository, ghOrgRepo GithubOrgRepo) Service { - return &service{ - repo: repo, +func NewService(gitV1Repository *v1Repositories.Repository, gitV2Repository RepositoryInterface, pcgRepo projects_cla_groups.Repository, ghOrgRepo github_organizations.RepositoryInterface, glOrgRepo GitLabOrgRepo, eventService events.Service) ServiceInterface { + return &Service{ + gitV1Repository: gitV1Repository, + gitV2Repository: gitV2Repository, projectsClaGroupsRepo: pcgRepo, ghOrgRepo: ghOrgRepo, + glOrgRepo: glOrgRepo, + eventService: eventService, + gitLabApp: gitLabApi.Init(config.GetConfig().Gitlab.AppClientID, config.GetConfig().Gitlab.AppClientSecret, config.GetConfig().Gitlab.AppPrivateKey), } } -func (s *service) AddGithubRepository(ctx context.Context, projectSFID string, input *models.GithubRepositoryInput) (*v1Models.GithubRepository, error) { +// GitHubAddRepositories adds the specified GitHub repository to the specified project +func (s *Service) GitHubAddRepositories(ctx context.Context, projectSFID string, input *models.GithubRepositoryInput) ([]*v1Models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "AddGitHubRepository", + "functionName": "v2.repositories.service.GitHubAddRepositories", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "claGroupID": utils.StringValue(input.ClaGroupID), "githubOrganizationName": utils.StringValue(input.GithubOrganizationName), - "repositoryGitHubID": utils.StringValue(input.RepositoryGithubID), + "repositoryGitHubID": input.RepositoryGithubID, + "repositoryGithubIds": input.RepositoryGithubIds, } + + log.WithFields(f).Debugf("loading project by SFID: %s", projectSFID) psc := v2ProjectService.GetClient() project, err := psc.GetProject(projectSFID) if err != nil { - log.WithFields(f).WithError(err).Warn("unable to load projectSFID") + log.WithFields(f).WithError(err).Warn("unable to load projectSFID from the platform project service") return nil, err } - var externalProjectID string - if project.Parent == "" || project.Parent == utils.TheLinuxFoundation { - externalProjectID = projectSFID + + var parentProjectSFID string + if !utils.IsProjectHaveParent(project) || utils.IsProjectHasRootParent(project) || utils.GetProjectParentSFID(project) == "" { + parentProjectSFID = projectSFID } else { - externalProjectID = project.Parent + parentProjectSFID = utils.GetProjectParentSFID(project) } - allMappings, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(aws.StringValue(input.ClaGroupID)) + + allMappings, err := s.projectsClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, aws.StringValue(input.ClaGroupID)) if err != nil { log.WithFields(f).WithError(err).Warn("unable to get project IDs for CLA Group") return nil, err @@ -105,46 +156,130 @@ func (s *service) AddGithubRepository(ctx context.Context, projectSFID string, i if !valid { return nil, fmt.Errorf("provided cla group id %s is not linked to project sfid %s", utils.StringValue(input.ClaGroupID), projectSFID) } - org, err := s.ghOrgRepo.GetGithubOrganizationByName(ctx, utils.StringValue(input.GithubOrganizationName)) + + org, err := s.ghOrgRepo.GetGitHubOrganizationByName(ctx, utils.StringValue(input.GithubOrganizationName)) if err != nil { log.WithFields(f).WithError(err).Warn("unable to get organization by name") return nil, err } - if len(org.List) == 0 { - return nil, errors.New("github app not installed on github organization") - } - repoGithubID, err := strconv.ParseInt(utils.StringValue(input.RepositoryGithubID), 10, 64) - if err != nil { - log.WithFields(f).WithError(err).Warn("unable to convert repository github ID to an integer - invalid value") - return nil, err - } - ghRepo, err := github.GetRepositoryByExternalID(ctx, org.List[0].OrganizationInstallationID, repoGithubID) - if err != nil { - log.WithFields(f).WithError(err).Warn("unable to get repository by external ID") - return nil, err + + // Updated to process a list of repository IDs - take the list (may be empty) and add the single repository GH ID if it was set + repositoryIDList := input.RepositoryGithubIds + if input.RepositoryGithubID != "" { + repositoryIDList = append(repositoryIDList, input.RepositoryGithubID) } - in := &v1Models.GithubRepositoryInput{ - RepositoryExternalID: input.RepositoryGithubID, - RepositoryName: ghRepo.FullName, - RepositoryOrganizationName: input.GithubOrganizationName, - RepositoryProjectID: input.ClaGroupID, - RepositoryType: aws.String("github"), - RepositoryURL: ghRepo.HTMLURL, + + // Remove any silly duplicates that may come + repositoryIDList = utils.RemoveDuplicates(repositoryIDList) + + var response []*v1Models.GithubRepository + + // For each repository ID provided... + // If this is slow, may want to optimize by making separate go routines for each item in the list + for _, repoID := range repositoryIDList { + // Convert the string value to an integer + repoGithubID, err := strconv.ParseInt(repoID, 10, 64) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert repository github ID %s to an integer - invalid value", repoID) + return nil, err + } + + log.WithFields(f).Debugf("loading GitHub repository by external id: %d", repoGithubID) + ghRepo, err := github.GetRepositoryByExternalID(ctx, org.List[0].OrganizationInstallationID, repoGithubID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to load repository by external ID: %d", repoGithubID) + return nil, err + } + f["repositoryName"] = ghRepo.FullName + f["repositoryURL"] = ghRepo.URL + f["repositoryGitHubID"] = repoGithubID + log.WithFields(f).Debugf("loaded GitHub repository by external id: %d - url: %s", repoGithubID, utils.StringValue(ghRepo.URL)) + + // Check if this repository exists in our database + log.WithFields(f).Debugf("checking if GitHub repository by name: %s exists...", utils.StringValue(ghRepo.FullName)) + existingRepositoryModel, lookupErr := s.GitHubGetRepositoryByName(ctx, utils.StringValue(ghRepo.FullName)) + if lookupErr != nil { + // If we have the repository not found error - this is ok - we are expecting this + if notFoundErr, ok := lookupErr.(*utils.GitHubRepositoryNotFound); ok { + log.WithFields(f).WithError(notFoundErr).Debugf("GitHub repository lookup didn't find a match for existing repository name: %s - ok to create", utils.StringValue(ghRepo.FullName)) + } else { + // Some other error - not good... + log.WithFields(f).WithError(lookupErr).Warnf("GitHub repository lookup failed for repository name: %s", utils.StringValue(ghRepo.FullName)) + return nil, lookupErr + } + } + + // We already have an existing repository model with the same name + if existingRepositoryModel != nil { + if !existingRepositoryModel.Enabled { + msg := fmt.Sprintf("Github repository: %s previously disabled - will re-enabled... ", utils.StringValue(ghRepo.FullName)) + log.WithFields(f).Debug(msg) + enabled := true + + _, now := utils.CurrentTime() + + log.WithFields(f).Debugf("Updating GitHub repository - setting enabled: true, OrgName: %s, CLA Group ID: %s", + utils.StringValue(input.GithubOrganizationName), utils.StringValue(input.ClaGroupID)) + v1Input := &v1Models.GithubRepositoryInput{ + Enabled: &enabled, + RepositoryOrganizationName: input.GithubOrganizationName, + RepositoryProjectID: input.ClaGroupID, + Note: fmt.Sprintf("re-enabling repository on %s.", now), + } + + // Update Repo details in case of any changes + updatedRepository, updateErr := s.gitV1Repository.GitHubUpdateRepository(ctx, existingRepositoryModel.RepositoryID, projectSFID, parentProjectSFID, v1Input) + if updateErr != nil { + log.WithFields(f).WithError(updateErr).Warnf("unable to update GitHub repository with name: %s, id: %s, using input: %+v", utils.StringValue(ghRepo.FullName), existingRepositoryModel.RepositoryID, v1Input) + return nil, updateErr + } + + // Append the results to our response model + response = append(response, updatedRepository) + } else { + log.WithFields(f).Warnf("GitHub repository already exists with repository name: %s and is already enabled - skipping update", utils.StringValue(ghRepo.FullName)) + continue + } + } else { + // No record exists... + log.WithFields(f).Debug("no existing GitHub repository configured - creating...") + in := &v1Models.GithubRepositoryInput{ + RepositoryExternalID: &repoID, // nolint + RepositoryName: ghRepo.FullName, + RepositoryOrganizationName: input.GithubOrganizationName, + RepositoryProjectID: input.ClaGroupID, + RepositoryType: aws.String("github"), + RepositoryURL: ghRepo.HTMLURL, + } + + addedModel, addErr := s.gitV1Repository.GitHubAddRepository(ctx, parentProjectSFID, projectSFID, in) + if addErr != nil { + log.WithFields(f).WithError(addErr).Warnf("unable to add github repository: %s for project: %s", *ghRepo.FullName, projectSFID) + return nil, addErr + } + + // Append the results to our response model + response = append(response, addedModel) + } } - return s.repo.AddGithubRepository(ctx, externalProjectID, projectSFID, in) + + return response, nil } -func (s *service) EnableRepository(ctx context.Context, repositoryID string) error { - return s.repo.EnableRepository(ctx, repositoryID) +// GitHubEnableRepository service function +func (s *Service) GitHubEnableRepository(ctx context.Context, repositoryID string) error { + return s.gitV1Repository.GitHubEnableRepository(ctx, repositoryID) } -func (s *service) DisableRepository(ctx context.Context, repositoryID string) error { - return s.repo.DisableRepository(ctx, repositoryID) +// GitHubDisableRepository service function +func (s *Service) GitHubDisableRepository(ctx context.Context, repositoryID string) error { + return s.gitV1Repository.GitHubDisableRepository(ctx, repositoryID) } -func (s *service) ListProjectRepositories(ctx context.Context, projectSFID string) (*v1Models.ListGithubRepositories, error) { +// GitHubListProjectRepositories service function +func (s *Service) GitHubListProjectRepositories(ctx context.Context, projectSFID string) (*v1Models.GithubListRepositories, error) { f := logrus.Fields{ - "functionName": "ListProjectRepositories", + "functionName": "v2.repositories.service.GitHubListProjectRepositories", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, } @@ -161,23 +296,23 @@ func (s *service) ListProjectRepositories(ctx context.Context, projectSFID strin return nil, err } f["projectName"] = projectModel.Name - if projectModel.Parent != "" { - f["projectParentSFID"] = projectModel.Parent + if utils.IsProjectHaveParent(projectModel) { + f["projectParentSFID"] = utils.GetProjectParentSFID(projectModel) } log.WithFields(f).Debug("loaded project from the project service") enabled := true - return s.repo.ListProjectRepositories(ctx, "", projectSFID, &enabled) + return s.gitV1Repository.GitHubListProjectRepositories(ctx, projectSFID, &enabled) //// Lookup orgs via projectSFID //log.WithFields(f).Debug("querying EasyCLA for organizations by project id...") //var githubOrgList *v1Models.GithubOrganizations - //githubOrgList, err = s.ghOrgRepo.GetGithubOrganizations(ctx, projectSFID) + //githubOrgList, err = s.ghOrgRepo.GetGitHubOrganizations(ctx, projectSFID) //if err != nil { // log.WithFields(f).WithError(err).Warn("unable to lookup project by id in the github organization table") // if projectModel.Parent != "" { // log.WithFields(f).Debugf("querying for organizations by parent project id: %s...", projectModel.Parent) // var ghOrgErr error - // githubOrgList, ghOrgErr = s.ghOrgRepo.GetGithubOrganizations(ctx, projectModel.Parent) + // githubOrgList, ghOrgErr = s.ghOrgRepo.GetGitHubOrganizations(ctx, projectModel.Parent) // if ghOrgErr != nil { // log.WithFields(f).WithError(ghOrgErr).Warn("unable to lookup project by parent id in the github organization table") // return nil, ghOrgErr @@ -186,7 +321,7 @@ func (s *service) ListProjectRepositories(ctx context.Context, projectSFID strin //} // //// Our response - empty to start with - //response := &v1Models.ListGithubRepositories{ + //response := &v1Models.GithubListRepositories{ // List: []*v1Models.GithubRepository{}, //} // @@ -223,7 +358,7 @@ func (s *service) ListProjectRepositories(ctx context.Context, projectSFID strin //} // //// Now, query our DB.... - //listOurGitHubRepos, err := s.repo.ListProjectRepositories(ctx, "", projectSFID, true) + //listOurGitHubRepos, err := s.gitV1Repository.GitHubListProjectRepositories(ctx, "", projectSFID, true) //if err != nil { // log.WithFields(f).WithError(err).Warn("unable to lookup repository records by id in our repositories table ") // return response, err @@ -233,7 +368,7 @@ func (s *service) ListProjectRepositories(ctx context.Context, projectSFID strin // return response, err //} // - //// For each repo that we have... + //// For each gitV1Repository that we have... //for _, ourGitHubRepo := range listOurGitHubRepos.List { // // Inefficient, but ok if the number of repos is relatively small // for _, r := range response.List { @@ -253,16 +388,29 @@ func (s *service) ListProjectRepositories(ctx context.Context, projectSFID strin //return response, nil } -func (s *service) GetRepository(ctx context.Context, repositoryID string) (*v1Models.GithubRepository, error) { - return s.repo.GetRepository(ctx, repositoryID) +// GitHubGetRepository service function +func (s *Service) GitHubGetRepository(ctx context.Context, repositoryID string) (*v1Models.GithubRepository, error) { + return s.gitV1Repository.GitHubGetRepository(ctx, repositoryID) } -func (s *service) GetProtectedBranch(ctx context.Context, projectSFID, repositoryID string) (*v2Models.GithubRepositoryBranchProtection, error) { +// GitHubGetRepositoryByName service function +func (s *Service) GitHubGetRepositoryByName(ctx context.Context, repositoryName string) (*v1Models.GithubRepository, error) { + return s.gitV1Repository.GitHubGetRepositoryByName(ctx, repositoryName) +} + +// GitHubGetRepositoryByExternalID service function +func (s *Service) GitHubGetRepositoryByExternalID(ctx context.Context, repositoryExternalID string) (*v1Models.GithubRepository, error) { + return s.gitV1Repository.GitHubGetRepositoryByExternalID(ctx, repositoryExternalID) +} + +// GitHubGetProtectedBranch service function +func (s *Service) GitHubGetProtectedBranch(ctx context.Context, projectSFID, repositoryID, branchName string) (*v2Models.GithubRepositoryBranchProtection, error) { f := logrus.Fields{ - "functionName": "repositories.service.GetProtectedBranch", + "functionName": "v2.repositories.service.GitHubGetProtectedBranch", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "repositoryID": repositoryID, + "branchName": branchName, } githubRepository, err := s.getGithubRepo(ctx, projectSFID, repositoryID) @@ -273,15 +421,14 @@ func (s *service) GetProtectedBranch(ctx context.Context, projectSFID, repositor githubOrgName := githubRepository.RepositoryOrganizationName githubRepoName := githubRepository.RepositoryName - githubRepoName = github.CleanGithubRepoName(githubRepoName) + githubRepoName = branch_protection.CleanGithubRepoName(githubRepoName) - githubClient, err := s.getGithubClientForOrgName(ctx, githubOrgName) + branchProtectionRepository, err := s.getBranchProtectionRepositoryForOrgName(ctx, githubOrgName) if err != nil { return nil, err } - branchProtectionRepository := github.NewBranchProtectionRepository(githubClient.Repositories, github.EnableNonBlockingLimiter()) - owner, branchName, err := s.getGithubOwnerBranchName(ctx, branchProtectionRepository, githubOrgName, githubRepoName) + owner, err := s.getGithubOwner(ctx, branchProtectionRepository, githubOrgName, githubRepoName) if err != nil { return nil, err } @@ -292,15 +439,15 @@ func (s *service) GetProtectedBranch(ctx context.Context, projectSFID, repositor branchProtection, err := branchProtectionRepository.GetProtectedBranch(ctx, owner, githubRepoName, branchName) if err != nil { - if errors.Is(err, github.ErrBranchNotProtected) { + if errors.Is(err, branch_protection.ErrBranchNotProtected) { return result, nil } - log.WithFields(f).WithError(err).Warnf("getting the github protected branch for owner : %s, repo : %s and branch : %s failed : %v", owner, githubRepoName, branchName, err) + log.WithFields(f).WithError(err).Warnf("getting the github protected branch for owner : %s, gitV1Repository : %s and branch : %s failed : %v", owner, githubRepoName, branchName, err) return nil, err } result.ProtectionEnabled = true - if github.IsEnforceAdminEnabled(branchProtection) { + if branch_protection.IsEnforceAdminEnabled(branchProtection) { result.EnforceAdmin = true } @@ -311,9 +458,10 @@ func (s *service) GetProtectedBranch(ctx context.Context, projectSFID, repositor return result, nil } -func (s *service) UpdateProtectedBranch(ctx context.Context, projectSFID, repositoryID string, input *v2Models.GithubRepositoryBranchProtectionInput) (*v2Models.GithubRepositoryBranchProtection, error) { +// GitHubUpdateProtectedBranch service function +func (s *Service) GitHubUpdateProtectedBranch(ctx context.Context, projectSFID, repositoryID string, input *v2Models.GithubRepositoryBranchProtectionInput) (*v2Models.GithubRepositoryBranchProtection, error) { f := logrus.Fields{ - "functionName": "repositories.service.UpdateProtectedBranch", + "functionName": "v2.repositories.service.GitHubUpdateProtectedBranch", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "repositoryID": repositoryID, @@ -328,22 +476,26 @@ func (s *service) UpdateProtectedBranch(ctx context.Context, projectSFID, reposi githubOrgName := githubRepository.RepositoryOrganizationName githubRepoName := githubRepository.RepositoryName - githubRepoName = github.CleanGithubRepoName(githubRepoName) + githubRepoName = branch_protection.CleanGithubRepoName(githubRepoName) - githubClient, err := s.getGithubClientForOrgName(ctx, githubOrgName) + branchProtectionRepository, err := s.getBranchProtectionRepositoryForOrgName(ctx, githubOrgName) if err != nil { log.WithFields(f).WithError(err).Warn("problem locating github client for organization name") return nil, err } - branchProtectionRepository := github.NewBranchProtectionRepository(githubClient.Repositories, github.EnableNonBlockingLimiter()) - owner, branchName, err := s.getGithubOwnerBranchName(ctx, branchProtectionRepository, githubOrgName, githubRepoName) + branchName := input.BranchName + if branchName == "" { + branchName = branch_protection.DefaultBranchName + } + + owner, err := s.getGithubOwner(ctx, branchProtectionRepository, githubOrgName, githubRepoName) if err != nil { log.WithFields(f).WithError(err).Warn("problem locating github owner branch name") return nil, err } f["owner"] = owner - f["branchName"] = branchName + f["branchName"] = input.BranchName var requiredChecks []string var disabledChecks []string @@ -379,12 +531,13 @@ func (s *service) UpdateProtectedBranch(ctx context.Context, projectSFID, reposi return nil, err } - return s.GetProtectedBranch(ctx, projectSFID, repositoryID) + return s.GitHubGetProtectedBranch(ctx, projectSFID, repositoryID, branchName) } -func (s *service) getGithubRepo(ctx context.Context, projectSFID, repositoryID string) (*v1Models.GithubRepository, error) { +// getGithubRepo service function +func (s *Service) getGithubRepo(ctx context.Context, projectSFID, repositoryID string) (*v1Models.GithubRepository, error) { f := logrus.Fields{ - "functionName": "repositories.service.getGitHubRepo", + "functionName": "v2.repositories.service.getGitHubRepo", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "projectSFID": projectSFID, "repositoryID": repositoryID, @@ -395,14 +548,14 @@ func (s *service) getGithubRepo(ctx context.Context, projectSFID, repositoryID s if err != nil { return nil, err } - githubRepository, err := s.GetRepository(ctx, repositoryID) + githubRepository, err := s.GitHubGetRepository(ctx, repositoryID) if err != nil { log.WithFields(f).Warnf("fetching repository failed : %s : %v", repositoryID, err) return nil, err } - // check if project and repo are actually associated - if githubRepository.ProjectSFID != projectSFID { + // check if project and gitV1Repository are actually associated + if githubRepository.RepositoryProjectSfid != projectSFID { msg := fmt.Sprintf("github repository %s doesn't belong to project : %s", repositoryID, projectSFID) log.WithFields(f).Warn(msg) return nil, errors.New(msg) @@ -411,54 +564,49 @@ func (s *service) getGithubRepo(ctx context.Context, projectSFID, repositoryID s return githubRepository, nil } -func (s *service) getGithubClientForOrgName(ctx context.Context, githubOrgName string) (*githubsdk.Client, error) { +// getBranchProtectionRepositoryForOrgName service function +func (s *Service) getBranchProtectionRepositoryForOrgName(ctx context.Context, githubOrgName string) (*branch_protection.BranchProtectionRepository, error) { f := logrus.Fields{ - "functionName": "repositories.service.getGitHubClientForOrgName", + "functionName": "v2.repositories.service.getGitHubClientForOrgName", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "githubOrgName": githubOrgName, } - githubOrg, err := s.ghOrgRepo.GetGithubOrganization(ctx, githubOrgName) + githubOrg, err := s.ghOrgRepo.GetGitHubOrganizationByName(ctx, githubOrgName) if err != nil { log.WithFields(f).Warnf("fetching githubOrg %s failed, error: %v", githubOrgName, err) return nil, err } + if len(githubOrg.List) == 0 { + return nil, errors.New("github app not installed on github organization") + } - githubClient, err := github.NewGithubAppClient(githubOrg.OrganizationInstallationID) + branchProtectionRepo, err := branch_protection.NewBranchProtectionRepository(githubOrg.List[0].OrganizationInstallationID, branch_protection.EnableNonBlockingLimiter()) if err != nil { - log.WithFields(f).Warnf("creating the github client for installation id %d failed, error: %v", githubOrg.OrganizationInstallationID, err) return nil, err } - - return githubClient, nil + return branchProtectionRepo, nil } -func (s *service) getGithubOwnerBranchName(ctx context.Context, branchProtectionRepository *github.BranchProtectionRepository, githubOrgName, githubRepoName string) (string, string, error) { +// getGithubOwner service function +func (s *Service) getGithubOwner(ctx context.Context, branchProtectionRepository *branch_protection.BranchProtectionRepository, githubOrgName, githubRepoName string) (string, error) { owner, err := branchProtectionRepository.GetOwnerName(ctx, githubOrgName, githubRepoName) if err != nil { - log.Warnf("getting the owner name for org : %s and repo : %s failed : %v", githubOrgName, githubRepoName, err) - return "", "", err + log.Warnf("getting the owner name for org : %s and gitV1Repository : %s failed : %v", githubOrgName, githubRepoName, err) + return "", err } if owner == "" { - log.Warnf("GitHub returned empty owner name for org : %s and repo : %s", githubOrgName, githubRepoName) - return "", "", fmt.Errorf("empty owner name") - } - - log.Debugf("getGitHubOwnerBranchName : owner of the repo : %s found : %s", owner, githubRepoName) - branchName, err := branchProtectionRepository.GetDefaultBranchForRepo(ctx, owner, githubRepoName) - if err != nil { - log.Warnf("getting default GitHub branch failed for owner : %s and repo : %s : %v", owner, githubRepoName, err) - return "", "", err + log.Warnf("GitHub returned empty owner name for org : %s and gitV1Repository : %s", githubOrgName, githubRepoName) + return "", fmt.Errorf("empty owner name") } - - return owner, branchName, nil + return owner, nil } -// getRequiredProtectedBranchCheckStatus -func (s *service) getRequiredProtectedBranchCheckStatus(protectedBranch *githubsdk.Protection, requiredChecks []string) []*v2Models.GithubRepositoryBranchProtectionStatusChecks { +// getRequiredProtectedBranchCheckStatus service function to get the required protected branch check status +func (s *Service) getRequiredProtectedBranchCheckStatus(branchProtectionRule *branch_protection.BranchProtectionRule, requiredChecks []string) []*v2Models.GithubRepositoryBranchProtectionStatusChecks { f := logrus.Fields{ - "functionName": "repositories.service.getRequiredProtectedBranchCheckStatus", + "functionName": "v2.repositories.service.getRequiredProtectedBranchCheckStatus", } log.WithFields(f).Debug("querying GitHub for status checks...") @@ -471,11 +619,11 @@ func (s *service) getRequiredProtectedBranchCheckStatus(protectedBranch *githubs }) resultMap[rc] = true } - if protectedBranch.RequiredStatusChecks == nil || len(protectedBranch.RequiredStatusChecks.Contexts) == 0 { + if len(branchProtectionRule.RequiredStatusCheckContexts) == 0 { return result } - for _, existingCheck := range protectedBranch.RequiredStatusChecks.Contexts { + for _, existingCheck := range branchProtectionRule.RequiredStatusCheckContexts { if !resultMap[existingCheck] { continue } @@ -487,19 +635,19 @@ func (s *service) getRequiredProtectedBranchCheckStatus(protectedBranch *githubs } } } - return result } -func (s *service) DisableCLAGroupRepositories(ctx context.Context, claGroupID string) error { +// GitHubDisableCLAGroupRepositories service function to disable CLA group repositories +func (s *Service) GitHubDisableCLAGroupRepositories(ctx context.Context, claGroupID string) error { f := logrus.Fields{ - "functionName": "DisableCLAGroupRepositories", + "functionName": "v2.repositories.service.GitHubDisableCLAGroupRepositories", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), "claGroupID": claGroupID, } var deleteErr error - ghOrgs, err := s.repo.GetCLAGroupRepositoriesGroupByOrgs(ctx, claGroupID, true) + ghOrgs, err := s.gitV1Repository.GitHubGetCLAGroupRepositoriesGroupByOrgs(ctx, claGroupID, true) if err != nil { return err } @@ -507,7 +655,7 @@ func (s *service) DisableCLAGroupRepositories(ctx context.Context, claGroupID st log.WithFields(f).Debugf("Deleting repositories for cla-group :%s", claGroupID) for _, ghOrg := range ghOrgs { for _, item := range ghOrg.List { - deleteErr = s.repo.DisableRepository(ctx, item.RepositoryID) + deleteErr = s.gitV1Repository.GitHubDisableRepository(ctx, item.RepositoryID) if deleteErr != nil { log.WithFields(f).Warnf("Unable to remove repository: %s for project :%s error :%v", item.RepositoryID, claGroupID, deleteErr) } diff --git a/cla-backend-go/v2/sign/constants.go b/cla-backend-go/v2/sign/constants.go new file mode 100644 index 000000000..d8d31c2d8 --- /dev/null +++ b/cla-backend-go/v2/sign/constants.go @@ -0,0 +1,15 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package sign + +const ( + //Github is a constant for github + Github = "github" + + // Unknown is a constant for unknown + Unknown = "Unknown" + + // Gitlab is a constant for gitlab + Gitlab = "gitlab" +) diff --git a/cla-backend-go/v2/sign/docusign.go b/cla-backend-go/v2/sign/docusign.go new file mode 100644 index 000000000..a621ea504 --- /dev/null +++ b/cla-backend-go/v2/sign/docusign.go @@ -0,0 +1,691 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package sign + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +// getAccessToken retrieves an access token for the DocuSign API using a JWT assertion. +func (s *service) getAccessToken(ctx context.Context) (string, error) { + f := logrus.Fields{ + "functionName": "v2.getAccessToken", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + jwtAssertion, err := jwtToken(s.docsignPrivateKey) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem generating the JWT token") + return "", err + } + + // Create the request + tokenRequestBody := DocuSignGetTokenRequest{ + GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", + Assertion: jwtAssertion, + } + + tokenRequestBodyJSON, err := json.Marshal(tokenRequestBody) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem marshalling the token request body") + return "", err + } + + url := fmt.Sprintf("https://%s/oauth/token", utils.GetProperty("DOCUSIGN_AUTH_SERVER")) + req, err := http.NewRequest("POST", url, strings.NewReader(string(tokenRequestBodyJSON))) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating the HTTP request") + return "", err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + // Make the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return "", err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + // Parse the response + responsePayload, err := io.ReadAll(resp.Body) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return "", err + } + + if resp.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("problem making the HTTP request - status code: %d", resp.StatusCode) + return "", errors.New("problem making the HTTP request") + } + + var tokenResponse DocuSignGetTokenResponse + + err = json.Unmarshal(responsePayload, &tokenResponse) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response body") + return "", err + } + + return tokenResponse.AccessToken, nil + +} + +// Void envelope +func (s *service) VoidEnvelope(ctx context.Context, envelopeID, message string) error { + f := logrus.Fields{ + "functionName": "v2.VoidEnvelope", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "envelopeID": envelopeID, + "message": message, + } + + accessToken, err := s.getAccessToken(ctx) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem getting the access token") + return err + } + + voidRequest := struct { + VoidReason string `json:"voidReason"` + }{ + VoidReason: message, + } + + voidRequestJSON, err := json.Marshal(voidRequest) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem marshalling the void request") + return err + } + + url := fmt.Sprintf("%s/accounts/%s/envelopes/%s/void", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID) + + req, err := http.NewRequest("PUT", url, strings.NewReader(string(voidRequestJSON))) + + if err != nil { + return err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + return err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + _, err = io.ReadAll(resp.Body) + + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return errors.New("problem making the HTTP request") + } + + return nil + +} + +func (s *service) createEnvelope(ctx context.Context, payload *DocuSignEnvelopeRequest) (string, error) { + f := logrus.Fields{ + "functionName": "v2.createEnvelope", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + // Serialize the signRequest into JSON + requestJSON, err := json.Marshal(payload) + if err != nil { + return "", err + } + + log.WithFields(f).Debugf("sign request: %+v", string(requestJSON)) + + // Get the access token + accessToken, err := s.getAccessToken(ctx) + + if err != nil { + return "", err + } + + // Create the request + url := fmt.Sprintf("%s/accounts/%s/envelopes", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID")) + + req, err := http.NewRequest("POST", url, strings.NewReader(string(requestJSON))) + + if err != nil { + return "", err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return "", err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + responsePayload, err := io.ReadAll(resp.Body) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return "", err + } + + if resp.StatusCode != http.StatusCreated { + log.WithFields(f).Warnf("problem making the HTTP request - status code: %d - response : %s", resp.StatusCode, string(responsePayload)) + return "", errors.New("problem making the HTTP request") + } + + var response DocuSignEnvelopeResponse + + err = json.Unmarshal(responsePayload, &response) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response body") + return "", err + } + + return response.EnvelopeId, nil + +} + +func (s *service) addDocumentToEnvelope(ctx context.Context, envelopeID, documentName string, document []byte) error { + f := logrus.Fields{ + "functionName": "v2.addDocumentToEnvelope", + } + + const method = "PUT" + + // Get the access token + accessToken, err := s.getAccessToken(ctx) + + if err != nil { + return err + } + + url := fmt.Sprintf("%s/accounts/%s/envelopes/%s/documents/1", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID) + + log.WithFields(f).Debugf("url: %s", url) + + body := &bytes.Buffer{} + + writer := multipart.NewWriter(body) + + part, partErr := writer.CreateFormFile("file", documentName) + if partErr != nil { + return partErr + } + + _, copyErr := io.Copy(part, bytes.NewReader(document)) + + if copyErr != nil { + return copyErr + } + + closeErr := writer.Close() + if closeErr != nil { + return closeErr + } + + // create the http request + req, err := http.NewRequest(method, url, body) + if err != nil { + return err + } + + // Set headers + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Set("Content-Disposition", fmt.Sprintf("filename=\"%s\"", documentName)) + req.Header.Set("Content-Type", "application/pdf") + req.Header.Set("Accept", "application/json") + + log.WithFields(f).Debugf("adding document to envelope with url: %s %s", method, url) + + // Send HTTP request + client := &http.Client{} + resp, clientErr := client.Do(req) + if clientErr != nil { + log.WithFields(f).WithError(clientErr).Warnf("problem invoking envelope document upload request to %s %s", method, url) + return clientErr + } + + //log.WithFields(f).Debugf("response: %+v", resp) + responsePayload, readErr := io.ReadAll(resp.Body) + if readErr != nil { + log.WithFields(f).WithError(readErr).Warnf("problem reading response body %+v", resp.Body) + return readErr + } + + // Expecting a 200 response + if resp.StatusCode != 200 { + msg := fmt.Sprintf("problem invoking http %s request to %s - response status code is not 200: %d - response is: %+v", method, url, resp.StatusCode, string(responsePayload)) + log.WithFields(f).Warn(msg) + return errors.New(msg) + } + + defer func() { + closeErr = resp.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warnf("problem closing response body") + } + }() + + var documentUpdateResponseModel DocuSignUpdateDocumentResponse + unmarshalErr := json.Unmarshal(responsePayload, &documentUpdateResponseModel) + if unmarshalErr != nil { + log.WithFields(f).WithError(unmarshalErr).Warnf("problem unmarshalling document update to the envelope response model JSON data") + return unmarshalErr + } + + log.WithFields(f).Debugf("successfully added document to envelope response body, uri: %s, documentGuid: %s, response: %+v", documentUpdateResponseModel.Uri, documentUpdateResponseModel.DocumentIdGuid, documentUpdateResponseModel) + + return nil + +} + +func (s *service) getEnvelopeRecipients(ctx context.Context, envelopeID string) ([]Signer, error) { + f := logrus.Fields{ + "functionName": "v2.getEnvelopeRecipients", + "envelopeID": envelopeID, + } + + // Get the access token + accessToken, err := s.getAccessToken(ctx) + + if err != nil { + return nil, err + } + + log.WithFields(f).Debugf("access token: %s", accessToken) + + // Create the request + url := fmt.Sprintf("%s/accounts/%s/envelopes/%s/recipients", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID) + + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + log.WithFields(f).Debugf("%+v", err) + return nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return nil, err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + responsePayload, err := io.ReadAll(resp.Body) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("problem getting getting recipients ") + } + + var response *DocusignRecipientResponse + + err = json.Unmarshal(responsePayload, &response) + + if err != nil { + log.WithFields(f).Debugf("unable to unmarshall response: %+v", err) + return nil, err + } + + log.WithFields(f).Debugf("got %d recipients", len(response.Signers)) + + return response.Signers, nil +} + +// Function to create a DocuSign envelope +func (s *service) PrepareSignRequest(ctx context.Context, signRequest *DocuSignEnvelopeRequest) (*DocusignEnvelopeResponse, error) { + f := logrus.Fields{ + "functionName": "v2.PrepareSignRequest", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + // Serialize the signRequest into JSON + requestJSON, err := json.Marshal(signRequest) + if err != nil { + return nil, err + } + + // Get the access token + accessToken, err := s.getAccessToken(ctx) + + if err != nil { + return nil, err + } + + log.WithFields(f).Debugf("access token: %s", accessToken) + + // Create the request + url := fmt.Sprintf("%s/accounts/%s/envelopes", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID")) + + req, err := http.NewRequest("POST", url, strings.NewReader(string(requestJSON))) + + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return nil, err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + responsePayload, err := io.ReadAll(resp.Body) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return nil, err + } + + if resp.StatusCode != http.StatusCreated { + log.WithFields(f).Warnf("problem making the HTTP request - status code: %d - response : %s", resp.StatusCode, string(responsePayload)) + return nil, errors.New("problem making the HTTP request") + } + + var response DocusignEnvelopeResponse + + err = json.Unmarshal(responsePayload, &response) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response body") + return nil, err + } + + return &response, nil + +} + +// Define a struct to represent the response from the DocuSign API. +type RecipientViewResponse struct { + URL string `json:"url"` +} + +// GetSignURL fetches the signing URL for the specified envelope and recipient + +func (s *service) GetSignURL(email, recipientID, userName, clientUserId, envelopeID, returnURL string) (string, error) { + + f := logrus.Fields{ + "functionName": "v2.GetSignURL", + "recipientID": recipientID, + "returnURL": returnURL, + } + + // Get the access token + accessToken, err := s.getAccessToken(context.Background()) + + if err != nil { + return "", err + } + + // Create the request + + url := fmt.Sprintf("%s/accounts/%s/envelopes/%s/views/recipient", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID) + + viewRecipientRequest := DocusignRecipientView{ + Email: email, + Username: userName, + RecipientID: recipientID, + ReturnURL: returnURL, + AuthenticaionMethod: "None", + } + + if clientUserId != "" { + viewRecipientRequest.ClientUserId = clientUserId + } + + jsonRequest, err := json.Marshal(viewRecipientRequest) + + if err != nil { + log.WithFields(f).Debugf("unable to marshal http request") + return "", err + } + + log.WithFields(f).Debugf("payload: %s", string(jsonRequest)) + + req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonRequest))) + + if err != nil { + return "", err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.WithFields(f).Debugf("%+v", err) + return "", err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.WithFields(f).Debugf("%+v", err) + return "", err + } + + if resp.StatusCode != http.StatusCreated { + log.WithFields(f).Debugf("response: %+s and status code: %d", string(body), resp.StatusCode) + return "", errors.New("failed to get signing URL") + } + + var viewResponse RecipientViewResponse + if err := json.Unmarshal(body, &viewResponse); err != nil { + log.WithFields(f).Debug("failed to unmarshall response") + return "", err + } + + log.WithFields(f).Debugf("View response: %+v", viewResponse) + + return viewResponse.URL, nil +} + +func (s service) GetSignedDocument(ctx context.Context, envelopeID, documentID string) ([]byte, error) { + f := logrus.Fields{ + "functionName": "v2.getSignedDocument", + "envelopeID": envelopeID, + } + + // Get the access token + accessToken, err := s.getAccessToken(ctx) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem getting the access token") + return nil, err + } + + // Create the request + url := fmt.Sprintf("%s/accounts/%s/envelopes/%s/documents/%s", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID, documentID) + + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating the HTTP request") + return nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return nil, err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + responsePayload, err := io.ReadAll(resp.Body) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return nil, err + } + + if resp.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("problem making the HTTP request - status code: %d - response : %s", resp.StatusCode, string(responsePayload)) + return nil, errors.New("problem making the HTTP request") + } + + return responsePayload, nil + +} + +func (s *service) GetEnvelopeDocuments(ctx context.Context, envelopeID string) ([]DocuSignDocument, error) { + f := logrus.Fields{ + "functionName": "v2.GetEnvelopeDocuments", + "envelopeID": envelopeID, + } + + // Get the access token + accessToken, err := s.getAccessToken(ctx) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem getting the access token") + return nil, err + } + + // Create the request + url := fmt.Sprintf("%s/accounts/%s/envelopes/%s/documents", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID) + + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem creating the HTTP request") + return nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem making the HTTP request") + return nil, err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + responsePayload, err := io.ReadAll(resp.Body) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem reading the response body") + return nil, err + } + + if resp.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("problem making the HTTP request - status code: %d - response : %s", resp.StatusCode, string(responsePayload)) + return nil, errors.New("problem making the HTTP request") + } + + var response []DocuSignDocument + + err = json.Unmarshal(responsePayload, &response) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem unmarshalling the response body") + return nil, err + } + + return response, nil +} diff --git a/cla-backend-go/v2/sign/handlers.go b/cla-backend-go/v2/sign/handlers.go index cbcb846e1..b7b1d2cd6 100644 --- a/cla-backend-go/v2/sign/handlers.go +++ b/cla-backend-go/v2/sign/handlers.go @@ -4,12 +4,18 @@ package sign import ( + "bytes" "context" "errors" "fmt" + "io" + "net/http" "strings" + 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/users" + "github.com/sirupsen/logrus" "github.com/LF-Engineering/lfx-kit/auth" "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" @@ -20,21 +26,76 @@ import ( "github.com/go-openapi/runtime/middleware" ) +var ( + // payload is the payload for the docusign callback + iclaGitHubPayload []byte + cclaDocusignPayload []byte +) + +// DocusignMiddleware is used to get access to xml request body +func DocusignMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f := logrus.Fields{ + "functionName": "v2.sign.handlers.docusignMiddleware", + } + var err error + log.WithFields(f).Debug("docusign middleware...") + iclaGitHubPayload, err = io.ReadAll(r.Body) + if err != nil { + log.Warnf("unable to read request body") + return + } + r.Body.Close() + r.Body = io.NopCloser(bytes.NewBuffer(iclaGitHubPayload)) + log.WithFields(f).Debugf("docusign middleware...payload: %s", string(iclaGitHubPayload)) + // call the next middleware + next.ServeHTTP(w, r) + }) +} + +// CCLADocusignMiddleware used to set CCLA middleware +func CCLADocusignMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f := logrus.Fields{ + "functionName": "v2.sign.handlers.cclaDocusignMiddleware", + } + var err error + log.WithFields(f).Debug("docusign middleware...") + cclaDocusignPayload, err = io.ReadAll(r.Body) + if err != nil { + log.Warnf("unable to read request body") + return + } + r.Body.Close() + r.Body = io.NopCloser(bytes.NewBuffer(cclaDocusignPayload)) + log.WithFields(f).Debugf("docusign middleware...payload: %s", string(cclaDocusignPayload)) + // call the next middleware + next.ServeHTTP(w, r) + }) +} + // Configure API call -func Configure(api *operations.EasyclaAPI, service Service) { +func Configure(api *operations.EasyclaAPI, service Service, userService users.Service) { // Retrieve a list of available templates api.SignRequestCorporateSignatureHandler = sign.RequestCorporateSignatureHandlerFunc( func(params sign.RequestCorporateSignatureParams, user *auth.User) middleware.Responder { reqID := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + ctx := utils.ContextWithRequestAndUser(params.HTTPRequest.Context(), reqID, user) // nolint utils.SetAuthUserProperties(user, params.XUSERNAME, params.XEMAIL) - if !utils.IsUserAuthorizedForProjectOrganizationTree(user, utils.StringValue(params.Input.ProjectSfid), utils.StringValue(params.Input.CompanySfid), utils.DISALLOW_ADMIN_SCOPE) { - return sign.NewRequestCorporateSignatureForbidden().WithPayload(&models.ErrorResponse{ - Code: "403", - Message: fmt.Sprintf("EasyCLA - 403 Forbidden - user %s does not have access to Request Corporate Signature with Project|Organization scope of %s | %s", - user.UserName, utils.StringValue(params.Input.ProjectSfid), utils.StringValue(params.Input.CompanySfid)), - XRequestID: reqID, - }) + f := logrus.Fields{ + "functionName": "v2.sign.handlers.SignRequestCorporateSignatureHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "CompanyID": params.Input.CompanySfid, + "ProjectSFID": params.Input.ProjectSfid, + "authUserName": utils.StringValue(params.XUSERNAME), + "authUserEmail": utils.StringValue(params.XEMAIL), + } + + if !utils.IsUserAuthorizedForProjectOrganizationTree(ctx, user, utils.StringValue(params.Input.ProjectSfid), utils.StringValue(params.Input.CompanySfid), utils.DISALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Request Corporate Signature with Project|Organization scope tree of %s | %s - allow admin scope: false", + user.UserName, utils.StringValue(params.Input.ProjectSfid), utils.StringValue(params.Input.CompanySfid)) + log.WithFields(f).Warn(msg) + return sign.NewRequestCorporateSignatureForbidden().WithPayload(utils.ErrorResponseForbidden(reqID, msg)) } resp, err := service.RequestCorporateSignature(ctx, utils.StringValue(params.XUSERNAME), params.Authorization, params.Input) @@ -66,6 +127,123 @@ func Configure(api *operations.EasyclaAPI, service Service) { } return sign.NewRequestCorporateSignatureOK().WithPayload(resp) }) + + api.SignRequestIndividualSignatureHandler = sign.RequestIndividualSignatureHandlerFunc( + func(params sign.RequestIndividualSignatureParams) middleware.Responder { + reqId := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTIDKey, reqId) + f := logrus.Fields{ + "functionName": "v2.sign.handlers.SignRequestIndividualSignatureHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": params.Input.ProjectID, + "returnURL": params.Input.ReturnURL, + "returnURLType": params.Input.ReturnURLType, + "userID": params.Input.UserID, + } + var resp *models.IndividualSignatureOutput + var err error + var preferredEmail string + + if strings.ToLower(params.Input.ReturnURLType) == Github || strings.ToLower(params.Input.ReturnURLType) == Gitlab { + log.WithFields(f).Debug("fetching user emails") + user, userErr := userService.GetUser(*params.Input.UserID) + if userErr != nil { + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, userErr)) + } + if len(user.Emails) == 0 { + msg := "no emails found" + log.WithFields(f).Warn(msg) + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, errors.New(msg))) + } + preferredEmail = user.Emails[0] + log.WithFields(f).Debug("requesting individual signature for github/gitlab") + resp, err = service.RequestIndividualSignature(ctx, params.Input, preferredEmail) + } else if strings.ToLower(params.Input.ReturnURLType) == "gerrit" { + log.WithFields(f).Debug("requesting individual signature for gerrit") + resp, err = service.RequestIndividualSignatureGerrit(ctx, params.Input) + } else { + msg := fmt.Sprintf("invalid return URL type: %s", params.Input.ReturnURLType) + log.WithFields(f).Warn(msg) + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, errors.New(msg))) + } + if err != nil { + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, err)) + } + return sign.NewRequestIndividualSignatureOK().WithPayload(resp) + }) + + api.SignIclaCallbackGithubHandler = sign.IclaCallbackGithubHandlerFunc( + func(params sign.IclaCallbackGithubParams) middleware.Responder { + reqId := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTIDKey, reqId) + + f := logrus.Fields{ + "functionName": "v2.sign.handlers.SignIclaCallbackGithubHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + err := service.SignedIndividualCallbackGithub(ctx, iclaGitHubPayload, params.InstallationID, params.ChangeRequestID, params.GithubRepositoryID) + if err != nil { + return sign.NewIclaCallbackGithubBadRequest() + } + + log.WithFields(f).Debug("github callback") + // err := service.SignedIndividualCallbackGithub(ctx, payload, params.UserID) + return sign.NewCclaCallbackOK() + }) + + api.SignIclaCallbackGitlabHandler = sign.IclaCallbackGitlabHandlerFunc( + func(params sign.IclaCallbackGitlabParams) middleware.Responder { + reqId := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTIDKey, reqId) + f := logrus.Fields{ + "functionName": "v2.sign.handlers.SignIclaCallbackGitlabHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + log.WithFields(f).Debug("gitlab callback") + + err := service.SignedIndividualCallbackGitlab(ctx, iclaGitHubPayload, params.UserID, params.OrganizationID, params.GitlabRepositoryID, params.MergeRequestID) + if err != nil { + return sign.NewIclaCallbackGitlabBadRequest() + } + return sign.NewCclaCallbackOK() + }) + + api.SignIclaCallbackGerritHandler = sign.IclaCallbackGerritHandlerFunc( + func(params sign.IclaCallbackGerritParams) middleware.Responder { + reqId := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTIDKey, reqId) + f := logrus.Fields{ + "functionName": "v2.sign.handlers.SignIclaCallbackGerritHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + log.WithFields(f).Debug("gerrit callback") + + err := service.SignedIndividualCallbackGerrit(ctx, iclaGitHubPayload, params.UserID) + if err != nil { + return sign.NewIclaCallbackGerritBadRequest() + } + return sign.NewCclaCallbackOK() + }) + + api.SignCclaCallbackHandler = sign.CclaCallbackHandlerFunc( + func(params sign.CclaCallbackParams) middleware.Responder { + reqId := utils.GetRequestID(params.XREQUESTID) + ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTIDKey, reqId) + f := logrus.Fields{ + "functionName": "v2.sign.handlers.SignCclaCallbackHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + log.WithFields(f).Debug("ccla callback") + err := service.SignedCorporateCallback(ctx, cclaDocusignPayload, params.CompanyID, params.ProjectID) + if err != nil { + return sign.NewCclaCallbackBadRequest() + } + return sign.NewCclaCallbackOK() + }) + } type codedResponse interface { diff --git a/cla-backend-go/v2/sign/helpers.go b/cla-backend-go/v2/sign/helpers.go new file mode 100644 index 000000000..c9520326e --- /dev/null +++ b/cla-backend-go/v2/sign/helpers.go @@ -0,0 +1,223 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package sign + +import ( + "context" + "errors" + "fmt" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/github" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +// updateChangeRequest is a helper function that updates PR - typically after the docusign is completed +func (s service) updateChangeRequest(ctx context.Context, installationID, repositoryID, pullRequestID int64, projectID string) error { + f := logrus.Fields{ + "functionName": "v1.signatures.service.updateChangeRequest", + "repositoryID": repositoryID, + "pullRequestID": pullRequestID, + "projectID": projectID, + } + + githubRepository, ghErr := github.GetGitHubRepository(ctx, installationID, 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, installationID, 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.userService.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.userService.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.userService.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, installationID, int(pullRequestID), gitHubOrgName, gitHubRepoName, githubRepository.ID, *latestSHA, signed, unsigned, s.ClaV1ApiURL, s.claLandingPage, s.claLogoURL) + if updateErr != nil { + log.WithFields(f).Debugf("unable to update PR: %d", pullRequestID) + return updateErr + } + + return nil +} + +// 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.signatureService.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.GetCLAGroup(ctx, projectID) + if claGroupModelErr != nil { + log.WithFields(f).WithError(claGroupModelErr).Warnf("problem looking up project: %s", projectID) + return &hasSigned, &companyAffiliation, claGroupModelErr + } + + employeeSigned, err := s.signatureService.ProcessEmployeeSignature(ctx, companyModel, claGroupModel, user) + + if err != nil { + 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 &hasSigned, &companyAffiliation, nil +} diff --git a/cla-backend-go/v2/sign/jwt.go b/cla-backend-go/v2/sign/jwt.go new file mode 100644 index 000000000..60c9745e7 --- /dev/null +++ b/cla-backend-go/v2/sign/jwt.go @@ -0,0 +1,49 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package sign + +import ( + "time" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/golang-jwt/jwt" + "github.com/sirupsen/logrus" +) + +func jwtToken(docusignPrivateKey string) (string, error) { + f := logrus.Fields{ + "functionName": "v2.sign.jwtToken", + } + + claims := jwt.MapClaims{ + "iss": utils.GetProperty("DOCUSIGN_INTEGRATOR_KEY"), // integration key / client_id + "sub": utils.GetProperty("DOCUSIGN_USER_ID"), // user_id, in PROD should be the EasyCLA Admin user account + "aud": utils.GetProperty("DOCUSIGN_AUTH_SERVER"), // account.docusign.com or account-d.docusign.com (for dev) + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour).Unix(), // one hour appears to be the max, minus 60 seconds + "scope": "signature impersonation", + } + // log.WithFields(f).Debugf("claims: %+v", claims) + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + token.Header["alg"] = "RS256" + token.Header["typ"] = "JWT" + + privateKey, privateKeyErr := jwt.ParseRSAPrivateKeyFromPEM([]byte(docusignPrivateKey)) + if privateKeyErr != nil { + log.WithFields(f).WithError(privateKeyErr).Warnf("problem decoding docusign private key") + return "", privateKeyErr + } + // log.WithFields(f).Debugf("private key: %s", utils.GetProperty("DOCUSIGN_RSA_PRIVATE_KEY")) + + signedToken, signedTokenErr := token.SignedString(privateKey) + if signedTokenErr != nil { + log.WithFields(f).WithError(signedTokenErr).Warnf("problem generating the signed token") + } + // log.WithFields(f).Debugf("signed token: %s", signedToken) + + return signedToken, signedTokenErr +} diff --git a/cla-backend-go/v2/sign/models.go b/cla-backend-go/v2/sign/models.go new file mode 100644 index 000000000..9951f612d --- /dev/null +++ b/cla-backend-go/v2/sign/models.go @@ -0,0 +1,634 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package sign + +import ( + "database/sql" + "encoding/xml" +) + +// DocuSignGetTokenRequest is the request body for getting a token from DocuSign +type DocuSignGetTokenRequest struct { + GrantType string `json:"grant_type"` + Assertion string `json:"assertion"` +} + +// DocuSignGetTokenResponse is the response body for getting a token from DocuSign +type DocuSignGetTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +// DocuSignUserInfoResponse is the response body for getting user info from DocuSign +type DocuSignUserInfoResponse struct { + Sub string `json:"sub"` // holds the GUID API username of the user that is being impersonated + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + Created string `json:"created"` + Email string `json:"email"` + Accounts []struct { + AccountId string `json:"account_id"` + IsDefault bool `json:"is_default"` + AccountName string `json:"account_name"` + BaseUri string `json:"base_uri"` + } `json:"accounts"` +} + +// DocuSignEnvelopeRequest is the request body for an envelope from DocuSign, see: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/ +type DocuSignEnvelopeRequest struct { + EnvelopeId string `json:"envelopeId,omitempty"` // The envelope ID of the envelope + EnvelopeIdStamping string `json:"envelopeIdStamping,omitempty"` // When true, Envelope ID Stamping is enabled. After a document or attachment is stamped with an Envelope ID, the ID is seen by all recipients and becomes a permanent part of the document and cannot be removed. + TemplateId string `json:"templateId,omitempty"` // The ID of the template. If a value is not provided, DocuSign generates a value. + Documents []DocuSignDocument `json:"documents,omitempty"` // A data model containing details about the documents associated with the envelope + DocumentBase64 string `json:"documentBase64,omitempty"` // The document's bytes. This field can be used to include a base64 version of the document bytes within an envelope definition instead of sending the document using a multi-part HTTP request. The maximum document size is smaller if this field is used due to the overhead of the base64 encoding. + DocumentsCombinedUri string `json:"documentsCombinedUri,omitempty"` // The URI for retrieving all of the documents associated with the envelope as a single PDF file. + DocumentsUri string `json:"documentsUri,omitempty"` // The URI for retrieving all of the documents associated with the envelope as separate files. + EmailSubject string `json:"emailSubject,omitempty"` // EmailSubject - The subject line of the email message that is sent to all recipients. + EmailBlurb string `json:"emailBlurb,omitempty"` // EmailBlurb - This is the same as the email body. If specified it is included in email body for all envelope recipients. + Recipients DocuSignRecipientType `json:"recipients,omitempty"` + TemplateRoles []DocuSignTemplateRole `json:"templateRoles,omitempty"` + EventNotification DocuSignEventNotification `json:"eventNotification,omitempty"` + + /* Status + Indicates the envelope status. Valid values when creating an envelope are: + + created: The envelope is created as a draft. It can be modified and sent later. + sent: The envelope will be sent to the recipients after the envelope is created. + + You can query these additional statuses once the recipients have interacted with the envelope. + + completed: The recipients have finished working with the envelope: the documents are signed and all required tabs are filled in. + declined: The envelope has been declined by the recipients. + delivered: The envelope has been delivered to the recipients. + signed: The envelope has been signed by the recipients. + voided: The envelope is no longer valid and recipients cannot access or sign the envelope. + + */ + Status string `json:"status,omitempty"` +} + +// DocusignEnvelopeResponse +type DocusignEnvelopeResponse struct { + EnvelopeId string `json:"envelopeId,omitempty"` + Status string `json:"status,omitempty"` + StatusDateTime string `json:"statusDateTime,omitempty"` + Uri string `json:"uri,omitempty"` +} + +// DocuSignDocument is the data model for a document from DocuSign +type DocuSignDocument struct { + DocumentId string `json:"documentId,omitempty"` // Specifies the document ID of this document. This value is used by tabs to determine which document they appear in. + DocumentBase64 string `json:"documentBase64,omitempty"` // The document's bytes. This field can be used to include a base64 version of the document bytes within an envelope definition instead of sending the document using a multi-part HTTP request. The maximum document size is smaller if this field is used due to the overhead of the base64 encoding.0:w + FileExtension string `json:"fileExtension,omitempty"` // The file extension type of the document. Non-PDF documents are converted to PDF. If the document is not a PDF, fileExtension is required. If you try to upload a non-PDF document without a fileExtension, you will receive an "unable to load document" error message. The file extension type of the document. If the document is not a PDF it is converted to a PDF. + FileFormatHint string `json:"fileFormatHint,omitempty"` + IncludeInDownload string `json:"includeInDownload,omitempty"` // When set to true, the document is included in the combined document download. + Name string `json:"name,omitempty"` // The name of the document. This is the name that appears in the list of documents when managing an envelope. + Order string `json:"order,omitempty"` // The order in which to sort the results. Valid values are: asc, desc +} + +// DocuSignRecipientType is the data model for a recipient from DocuSign +type DocuSignRecipientType struct { + Agents []DocuSignRecipient `json:"agent,omitempty"` + CarbonCopies []DocuSignRecipient `json:"carbonCopy,omitempty"` + CertifiedDeliveries []DocuSignRecipient `json:"certifiedDelivery,omitempty"` + Editors []DocuSignRecipient `json:"editor,omitempty"` + InPersonSigners []DocuSignRecipient `json:"inPersonSigner,omitempty"` + Intermediaries []DocuSignRecipient `json:"intermediary,omitempty"` + Notaries []DocuSignRecipient `json:"notaryRecipient,omitempty"` + Participants []DocuSignRecipient `json:"participant,omitempty"` + Seals []DocuSignRecipient `json:"seals,omitempty"` // A list of electronic seals to apply to documents. + Signers []DocuSignRecipient `json:"signers,omitempty"` // A list of signers on the envelope. + Witnesses []DocuSignRecipient `json:"witness,omitempty"` // A list of signers who act as witnesses for an envelope. + RecipientCount string `json:"recipientCount,omitempty"` // The number of recipients in the envelope. +} + +// DocuSignRecipient is the data model for an editor or signer from DocuSign +type DocuSignRecipient struct { + RecipientId string `json:"recipientId,omitempty"` // Unique for the recipient. It is used by the tab element to indicate which recipient is to sign the document. + + ClientUserId string `json:"clientUserId,omitempty"` // Specifies whether the recipient is embedded or remote. If the clientUserId property is not null then the recipient is embedded. Use this field to associate the signer with their userId in your app. Authenticating the user is the responsibility of your app when you use embedded signing. + + /* The recipient type, as specified by the following values: + agent: Agent recipients can add name and email information for recipients that appear after the agent in routing order. + carbonCopy: Carbon copy recipients get a copy of the envelope but don't need to sign, initial, date, or add information to any of the documents. This type of recipient can be used in any routing order. + certifiedDelivery: Certified delivery recipients must receive the completed documents for the envelope to be completed. They don't need to sign, initial, date, or add information to any of the documents. + editor: Editors have the same management and access rights for the envelope as the sender. Editors can add name and email information, add or change the routing order, set authentication options, and can edit signature/initial tabs and data fields for the remaining recipients. + inPersonSigner: In-person recipients are DocuSign users who act as signing hosts in the same physical location as the signer. + intermediaries: Intermediary recipients can optionally add name and email information for recipients at the same or subsequent level in the routing order. + seal: Electronic seal recipients represent legal entities. + signer: Signers are recipients who must sign, initial, date, or add data to form fields on the documents in the envelope. + witness: Witnesses are recipients whose signatures affirm that the identified signers have signed the documents in the envelope. + */ + RecipientType string `json:"recipientType,omitempty"` + + RoleName string `json:"roleName,omitempty"` // Optional element. Specifies the role name associated with the recipient. This property is required when you are working with template recipients. + + RoutingOrder string `json:"routingOrder,omitempty"` // Specifies the routing order of the recipient in the envelope. + + Name string `json:"name,omitempty"` // The full legal name of the recipient. Maximum Length: 100 characters. Note: You must always set a value for this property in requests, even if firstName and lastName are set. + FirstName string `json:"firstName,omitempty"` // recipient's first name (50 characters maximum) + LastName string `json:"lastName,omitempty"` // recipient's last name + Email string `json:"email,omitempty"` // recipient's email address + Note string `json:"note,omitempty"` // A note sent to the recipient in the signing email. This note is unique to this recipient. In the user interface, it appears near the upper left corner of the document on the signing screen. Maximum Length: 1000 characters. + + Tabs DocuSignTab `json:"tabs"` // The tabs associated with the recipient. The tabs property enables you to programmatically position tabs on the document. For example, you can specify that the SIGN_HERE tab is placed at a given (x,y) location on the document. You can also specify the font, font color, font size, and other properties of the text in the tab. You can also specify the location and size of the tab. For example, you can specify that the tab is 50 pixels wide and 20 pixels high. You can also specify the page number on which the tab is located and whether the tab is located in a document, a template, or an inline template. For more information about tabs, see the Tabs section of the REST API documentation. +} + +// TextOptionalTab + +type TextOptionalTab struct { + Name string `json:"name"` + Value string `json:"value"` + Height int `json:"height"` + Width int `json:"width"` + Locked bool `json:"locked"` + Required bool `json:"required"` +} + +// DocuSignTab is the data model for a tab from DocuSign +type DocuSignTab struct { + ApproveTabs []DocuSignTabDetails `json:"approveTabs,omitempty"` + CheckBoxTabs []DocuSignTabDetails `json:"checkboxTabs,omitempty"` + CommentThreadTabs []DocuSignTabDetails `json:"commentThreadTabs,omitempty"` + CommissionCountyTabs []DocuSignTabDetails `json:"commissionCountyTabs,omitempty"` + CommissionExpirationTabs []DocuSignTabDetails `json:"commissionExpirationTabs,omitempty"` + CommissionNumberTabs []DocuSignTabDetails `json:"commissionNumberTabs,omitempty"` + CommissionStateTabs []DocuSignTabDetails `json:"commissionStateTabs,omitempty"` + CompanyTabs []DocuSignTabDetails `json:"companyTabs,omitempty"` + DateSignedTabs []DocuSignTabDetails `json:"dateSignedTabs,omitempty"` + DateTabs []DocuSignTabDetails `json:"dateTabs,omitempty"` + DeclinedTabs []DocuSignTabDetails `json:"declineTabs,omitempty"` + DrawTabs []DocuSignTabDetails `json:"drawTabs,omitempty"` + EmailAddressTabs []DocuSignTabDetails `json:"emailAddressTabs,omitempty"` + EmailTabs []DocuSignTabDetails `json:"emailTabs,omitempty"` + EnvelopeIdTabs []DocuSignTabDetails `json:"envelopeIdTabs,omitempty"` + FirstNameTabs []DocuSignTabDetails `json:"firstNameTabs,omitempty"` + FormulaTabs []DocuSignTabDetails `json:"formulaTab,omitempty"` + FullNameTabs []DocuSignTabDetails `json:"fullNameTabs,omitempty"` + InitialHereTabs []DocuSignTabDetails `json:"initialHereTabs,omitempty"` + LastNameTabs []DocuSignTabDetails `json:"lastNameTabs,omitempty"` + ListTabs []DocuSignTabDetails `json:"listTabs,omitempty"` + NotarizeTabs []DocuSignTabDetails `json:"notarizeTabs,omitempty"` + NotarySealTabs []DocuSignTabDetails `json:"notarySealTabs,omitempty"` + NoteTabs []DocuSignTabDetails `json:"noteTabs,omitempty"` + NumberTabs []DocuSignTabDetails `json:"numberTabs,omitempty"` + NumericalTabs []DocuSignTabDetails `json:"numericalTabs,omitempty"` + PhoneNumberTabs []DocuSignTabDetails `json:"phoneNumberTabs,omitempty"` + PolyLineOverlayTabs []DocuSignTabDetails `json:"polyLineOverlayTabs,omitempty"` + PrefillTabs []DocuSignTabDetails `json:"prefillTabs,omitempty"` + RadioGroupTabs []DocuSignTabDetails `json:"radioGroupTabs,omitempty"` + SignerAttachmentTabs []DocuSignTabDetails `json:"signerAttachmentTabs,omitempty"` + SignHereTabs []DocuSignTabDetails `json:"signHereTabs,omitempty"` + SmartSectionTabs []DocuSignTabDetails `json:"smartSectionTabs,omitempty"` + SSNTabs []DocuSignTabDetails `json:"ssnTabs,omitempty"` + TabGroups []DocuSignTabDetails `json:"tabGroupTabs,omitempty"` + TextTabs []DocuSignTabDetails `json:"textTabs,omitempty"` + TitleTabs []DocuSignTabDetails `json:"titleTabs,omitempty"` + ViewTabs []DocuSignTabDetails `json:"viewTabs,omitempty"` + ZipTabs []DocuSignTabDetails `json:"zipTabs,omitempty"` + TextOptionalTabs []DocuSignTabDetails `json:"textOptionalTabs,omitempty"` + SignHereOptionalTabs []DocuSignTabDetails `json:"signHereOptionalTabs,omitempty"` +} + +// DocuSignTabDetails is the data model for a tab from DocuSign +type DocuSignTabDetails struct { + AnchorCaseSensitive string `json:"anchorCaseSensitive,omitempty"` // anchor case sensitive flag, "true" or "false" + AnchorIgnoreIfNotPresent string `json:"anchorIgnoreIfNotPresent,omitempty"` // When true, this tab is ignored if the anchorString is not found in the document. + AnchorHorizontalAlignment string `json:"anchorHorizontalAlignment,omitempty"` // This property controls how anchor tabs are aligned in relation to the anchor text. Possible values are : left: Aligns the left side of the tab with the beginning of the first character of the matching anchor word. This is the default value. right: Aligns the tab’s left side with the last character of the matching anchor word. + AnchorMatchWholeWord string `json:"anchorMatchWholeWord,omitempty"` // When true, the text string in a document must match the value of the anchorString property in its entirety for an anchor tab to be created. The default value is false. For example, when set to true, if the input is man then man will match but manpower, fireman, and penmanship will not. When false, if the input is man then man, manpower, fireman, and penmanship will all match. + AnchorString string `json:"anchorString,omitempty"` // Specifies the string to find in the document and use as the basis for tab placement + AnchorUnits string `json:"anchorUnits,omitempty"` // anchor units, pixels, cms, mms + AnchorXOffset string `json:"anchorXOffset,omitempty"` // anchor x offset + AnchorYOffset string `json:"anchorYOffset,omitempty"` // anchor y offset + Bold string `json:"bold,omitempty"` // bold flag, "true" or "false" + DocumentId string `json:"documentId,omitempty"` // Specifies the document ID number that the tab is placed on. This must refer to an existing Document's ID attribute. + Font string `json:"font,omitempty"` // font + FontSize string `json:"fontSize,omitempty"` // font size + Height string `json:"height,omitempty"` // The height of the tab in pixels. Must be an integer. + Locked string `json:"locked,omitempty"` // locked flag, "true" or "false" + MinNumericalValue string `json:"minNumericalValue,omitempty"` // minimum numerical value, such as "0", used for validation of numerical tabs + MaxNumericalValue string `json:"maxNumericalValue,omitempty"` // maximum numerical value, such as "100", used for validation of numerical tabs + Name string `json:"name,omitempty"` // The name of the tab. For example, Sign Here or Initial Here. If the tooltip attribute is not set, this value will be displayed as the custom tooltip text. + Optional string `json:"optional,omitempty"` // When true, the recipient does not need to complete this tab to complete the signing process + PageNumber string `json:"pageNumber,omitempty"` // Specifies the page number on which the tab is located. Must be 1 for supplemental documents. + Required string `json:"required,omitempty"` // When true, the signer is required to fill out this tab + TabId string `json:"tabId,omitempty"` // tab idj + TabLabel string `json:"tabLabel,omitempty"` // label + TabOrder string `json:"tabOrder,omitempty"` // A positive integer that sets the order the tab is navigated to during signing. Tabs on a page are navigated to in ascending order, starting with the lowest number and moving to the highest. If two or more tabs have the same tabOrder value, the normal auto-navigation setting behavior for the envelope is used. + TabType string `json:"tabType,omitempty"` // Indicates type of tab (for example: signHere or initialHere) + ToolTip string `json:"toolTip,omitempty"` // The text of a tooltip that appears when a user hovers over a form field or tab. + Width string `json:"width,omitempty"` // The width of the tab in pixels. Must be an integer. This is not applicable to Sign Here tab. + XPosition string `json:"xPosition,omitempty"` // x position + YPosition string `json:"yPosition,omitempty"` // x position + ValidationType string `json:"validationType,omitempty"` // validation type, "string", "number", "date", "zipcode", "currency" + Value string `json:"value,omitempty"` + CustomTabId string `json:"customTabId,omitempty"` +} + +// DocuSignTemplateRole is the request body for a template role from DocuSign +type DocuSignTemplateRole struct { + Name string `json:"name,omitempty"` // the recipient's email address + Email string `json:"email,omitempty"` // the recipient's name + RoleName string `json:"roleName,omitempty"` // the template role name associated with the recipient + ClientUserID string `json:"clientUserId,omitempty"` // Specifies whether the recipient is embedded or remote. If the clientUserId property is not null then the recipient is embedded. Use this field to associate the signer with their userId in your app. Authenticating the user is the responsibility of your app when you use embedded signing. If the clientUserId property is set and either SignerMustHaveAccount or SignerMustLoginToSign property of the account settings is set to true, an error is generated on sending. + RoutingOrder string `json:"routingOrder,omitempty"` // Specifies the routing order of the recipient in the envelope. +} + +// DocuSignEnvelopeResponse is the response body for an envelope from DocuSign, see: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/update/ +type DocuSignEnvelopeResponse struct { + EnvelopeId string `json:"envelopeId,omitempty"` + Recipients []DocuSignRecipient `json:"recipients,omitempty"` + ErrorDetails struct { + ErrorCode string `json:"errorCode,omitempty"` + Message string `json:"message,omitempty"` + } `json:"errorDetails,omitempty"` +} + +// DocuSignEnvelopeResponseModel is the envelope response model +type DocuSignEnvelopeResponseModel struct { + /* + // Response from: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/get/ + { + "allowMarkup": "false", + "autoNavigation": "true", + "brandId": "56502fe1-xxxx-xxxx-xxxx-97cb5c43176a", + "certificateUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/documents/certificate", + "createdDateTime": "2016-10-05T01:04:58.1830000Z", + "customFieldsUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/custom_fields", + "documentsCombinedUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/documents/combined", + "documentsUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/documents", + "emailSubject": "Please sign the NDA", + "enableWetSign": "true", + "envelopeId": "4b728be4-xxxx-xxxx-xxxx-d63e23f822b6", + "envelopeIdStamping": "true", + "envelopeUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6", + "initialSentDateTime": "2016-10-05T01:04:58.7770000Z", + "is21CFRPart11": "false", + "isSignatureProviderEnvelope": "false", + "lastModifiedDateTime": "2016-10-05T01:04:58.1830000Z", + "notificationUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/notification", + "purgeState": "unpurged", + "recipientsUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/recipients", + "sentDateTime": "2016-10-05T01:04:58.7770000Z", + "status": "sent", + "statusChangedDateTime": "2016-10-05T01:04:58.7770000Z", + "templatesUri": "/envelopes/4b728be4-xxxx-xxxx-xxxx-d63e23f822b6/templates" + } + */ + AllowMarkup string `json:"allowMarkup,omitempty"` + AutoNavigation string `json:"autoNavigation,omitempty"` + BrandId string `json:"brandId,omitempty"` + CertificateUri string `json:"certificateUri,omitempty"` + CreatedDateTime string `json:"createdDateTime,omitempty"` + CustomFieldsUri string `json:"customFieldsUri,omitempty"` + DocumentsCombinedUri string `json:"documentsCombinedUri,omitempty"` + DocumentsUri string `json:"documentsUri,omitempty"` + EmailSubject string `json:"emailSubject,omitempty"` + EnableWetSign string `json:"enableWetSign,omitempty"` + EnvelopeId string `json:"envelopeId,omitempty"` + EnvelopeIdStamping string `json:"envelopeIdStamping,omitempty"` + EnvelopeUri string `json:"envelopeUri,omitempty"` + InitialSentDateTime string `json:"initialSentDateTime,omitempty"` + Is21CFRPart11 string `json:"is21CFRPart11,omitempty"` + IsSignatureProviderEnvelope string `json:"isSignatureProviderEnvelope,omitempty"` + LastModifiedDateTime string `json:"lastModifiedDateTime,omitempty"` + NotificationUri string `json:"notificationUri,omitempty"` + PurgeState string `json:"purgeState,omitempty"` + RecipientsUri string `json:"recipientsUri,omitempty"` + SentDateTime string `json:"sentDateTime,omitempty"` + Status string `json:"status,omitempty"` + StatusChangedDateTime string `json:"statusChangedDateTime,omitempty"` + TemplatesUri string `json:"templatesUri,omitempty"` +} + +// IndividualMembershipDocuSignDBSummaryModel is the data model for an individual membership DocuSign database summary models +type IndividualMembershipDocuSignDBSummaryModel struct { + DocuSignEnvelopeID string `db:"docusign_envelope_id"` + DocuSignEnvelopeCreatedAt string `db:"docusign_envelope_created_at"` + DocuSignEnvelopeSigningStatus string `db:"docusign_envelope_signing_status"` + DocuSignEnvelopeSigningUpdatedAt string `db:"docusign_envelope_signing_updated_at"` + Memo sql.NullString `db:"memo"` + //DocuSignEnvelopeSignedDate string `json:"docusign_envelope_signed_date"` +} + +type ClaSignatoryEmailParams struct { + ClaGroupName string + SignatoryName string + ClaManagerName string + ClaManagerEmail string + CompanyName string + ProjectVersion string + ProjectNames []string +} + +type DocuSignRecipientEvent struct { + EnvelopeEventStatusCode string `json:"envelopeEventStatusCode"` +} + +type DocuSignEventNotification struct { + URL string `json:"url"` + LoggingEnabled bool `json:"loggingEnabled"` + EnvelopeEvents []DocuSignRecipientEvent `json:"envelopeEvents"` + // EventData EventData `json:"eventData"` + // RequireAcknowledgment string `json:"requireAcknowledgment"` +} + +// EventData represents the eventData attribute in DocusignEventNotification. +type EventData struct { + Version string `json:"version,omitempty"` + Format string `json:"format,omitempty"` + IncludeData []string `json:"includeData,omitempty"` +} + +type Recipient struct { + Name string `json:"name"` + Email string `json:"email"` + // Other recipient-specific fields +} + +// DocuSignUpdateDocumentResponse is the response body for adding/updating a document to an envelope from DocuSign +type DocuSignUpdateDocumentResponse struct { + /* + {"documentId":"1","documentIdGuid":"2c205f31-4c6b-4237-b6bc-d79457b949a5","name":"document.pdf","type":"content","uri":"/envelopes/ebeee6a6-c17f-4d05-8441-38d5c1ad9675/documents/1","order":"1","containsPdfFormFields":"false","templateRequired":"false","authoritativeCopy":"false"} + */ + DocumentId string `json:"documentId,omitempty"` + DocumentIdGuid string `json:"documentIdGuid,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Uri string `json:"uri,omitempty"` + Order string `json:"order,omitempty"` + ContainsPdfFormFields string `json:"containsPdfFormFields,omitempty"` + TemplateRequired string `json:"templateRequired,omitempty"` + AuthoritativeCopy string `json:"authoritativeCopy,omitempty"` +} + +type DocuSignXMLData struct { + XMLName xml.Name `xml:"EnvelopeStatus"` + EnvelopeID string `xml:"EnvelopeID"` + Status string `xml:"Status"` + Subject string `xml:"Subject"` + UserName string `xml:"UserName"` + Email string `xml:"Email"` + SignedDateTime string `xml:"SignedDateTime"` + // Include other fields as necessary +} + +type Signer struct { + CreationReason string `json:"creationReason"` + IsBulkRecipient string `json:"isBulkRecipient"` + Name string `json:"name"` + Email string `json:"email"` + RecipientId string `json:"recipientId"` + RecipientIdGuid string `json:"recipientIdGuid"` + RequireIdLookup string `json:"requireIdLookup"` + UserId string `json:"userId"` + ClientUserId string `json:"clientUserId"` + RoutingOrder string `json:"routingOrder"` + RoleName string `json:"roleName"` + Status string `json:"status"` +} + +type DocusignRecipientResponse struct { + Signers []Signer `json:"signers"` +} + +type DocusignRecipientView struct { + Email string `json:"email"` + Username string `json:"userName"` + ReturnURL string `json:"returnUrl"` + RecipientID string `json:"recipientId"` + ClientUserId string `json:"clientUserId,omitempty"` + AuthenticaionMethod string `json:"authenticationMethod"` +} + +type DocusignRecipientViewResponse struct { + URL string `json:"url"` +} + +// DocuSignWebhookModel represents the webhook callback data model from DocuSign +type DocuSignWebhookModel struct { + APIVersion string `json:"apiVersion"` // v2.1 + ConfigurationID int `json:"configurationId"` // 10418598 + Data DocuSignWebhookData `json:"data"` + Event string `json:"event"` // envelope-sent, envelope-completed + GeneratedDateTime string `json:"generatedDateTime"` // generated_date_time + URI string `json:"uri"` // /restapi/v2.1/accounts/77c754e9-4016-4ccc-957f-15eaa18f2d22/envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a +} + +type DocuSignWebhookData struct { + AccountID string `json:"accountId"` // 77c754e9-4016-4ccc-957f-15eaa18f2d22 + EnvelopeID string `json:"envelopeId"` // 016d4678-bf5c-41f3-b7c9-5c58606cdb4a + EnvelopeSummary DocuSignEnvelopeSummary `json:"envelopeSummary"` + UserID string `json:"userId"` // 9fd66d5d-7396-4b80-a85e-2a7e536471b1 +} + +type DocuSignEnvelopeSummary struct { + AllowComments string `json:"allowComments"` // "true" + AllowMarkup string `json:"allowMarkup"` // "false" + AllowReassign string `json:"allowReassign"` // "true" + AllowViewHistory string `json:"allowViewHistory"` // "true" + AnySigner interface{} `json:"anySigner"` // + AttachmentsURI string `json:"attachmentsUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/attachments + AutoNavigation string `json:"autoNavigation"` // "true" + BurnDefaultTabData string `json:"burnDefaultTabData"` // "false" + CertificateURI string `json:"certificateUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/documents/summary + CreatedDateTime string `json:"createdDateTime"` // 2023-05-26T18:55:47.18Z + CustomFieldsURI string `json:"customFieldsUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/custom_fields + DocumentsCombinedURI string `json:"documentsCombinedUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/documents/combined + DocumentsURI string `json:"documentsUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/documents + EmailSubject string `json:"emailSubject"` // Please DocuSign this document: Test DocuSign + EnableWetSign string `json:"enableWetSign"` // "false" + EnvelopeID string `json:"envelopeId"` // 016d4678-bf5c-41f3-b7c9-5c58606cdb4a + EnvelopeIDStamping string `json:"envelopeIdStamping"` // "true" + EnvelopeLocation string `json:"envelopeLocation"` // current_site + EnvelopeMetadata EnvelopeMetadata `json:"envelopeMetadata"` + EnvelopeURI string `json:"envelopeUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a + ExpiresAfter string `json:"expiresAfter"` // 120 + ExpireDateTime string `json:"expireDateTime"` // 2023-05-26T18:55:48.257Z + ExpireEnabled string `json:"expireEnabled"` // "true" + HasComments string `json:"hasComments"` // "false" + HasFormDataChanged string `json:"hasFormDataChanged"` // "false" + InitialSendDateTime string `json:"initialSendDateTime"` // 2023-05-26T18:55:48.257Z + Is21CFRPart11 string `json:"is21CFRPart11"` // "false" + IsDynamicEnvelope string `json:"isDynamicEnvelope"` // "false" + IsSignatureProviderEnvelope string `json:"isSignatureProviderEnvelope"` // "false" + LastModifiedDateTime string `json:"lastModifiedDateTime"` // 2023-05-26T18:55:48.257Z + NotificationURI string `json:"notificationUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/notification + PurgeState string `json:"purgeState"` // unpurged + Recipients Recipients `json:"recipients"` + RecipientsURI string `json:"recipientsUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/recipients + Sender Sender `json:"sender"` + SentDateTime string `json:"sentDateTime"` // 2023-05-26T18:55:48.257Z + SignerCanSignOnMobile string `json:"signerCanSignOnMobile"` // "true" + SignerLocation string `json:"signerLocation"` // online + Status string `json:"status"` // sent + StatusChangedDateTime string `json:"statusChangedDateTime"` // 2023-05-26T18:55:48.257Z + TemplatesURI string `json:"templatesUri"` // /envelopes/016d4678-bf5c-41f3-b7c9-5c58606cdb4a/templates0:w +} + +type Recipients struct { + Agents []interface{} `json:"agents"` // + CarbonCopies []interface{} `json:"carbonCopies"` // + CertifiedDeliveries []interface{} `json:"certifiedDeliveries"` // + CurrentRoutingOrder string `json:"currentRoutingOrder"` // 1 + Editors []interface{} `json:"editors"` // + InPersonSigners []interface{} `json:"inPersonSigners"` // + Intermediaries []interface{} `json:"intermediaries"` // + Notaries []interface{} `json:"notaries"` // + RecipientCount string `json:"recipientCount"` // 1 + Seals []interface{} `json:"seals"` // + Signers []WebhookSigner `json:"signers"` + Witnesses []interface{} `json:"witnesses"` // +} + +type EnvelopeMetadata struct { + AllowAdvancedCorrect string `json:"allowAdvancedCorrect"` // "false" + AllowCorrect string `json:"allowCorrect"` // "true" + EnableSignWithNotary string `json:"enableSignWithNotary"` // "false" +} + +type WebhookSigner struct { + CompletedCount string `json:"completedCount"` // 0 + CreationReason string `json:"creationReason"` // sender + DeliveryMethod string `json:"deliveryMethod"` // email + Email string `json:"email"` // test@test + IsBulkRecipient string `json:"isBulkRecipient"` // "false" + Name string `json:"name"` // Test DocuSign + RecipientID string `json:"recipientId"` // 1 + RecipientIDGuid string `json:"recipientIdGuid"` // 9fd66d5d-7396-4b80-a85e-2a7e536471b1 + ReceipientType string `json:"recipientType"` // signer + RequireIdLookup string `json:"requireIdLookup"` // "false" + RequireUploadSignature string `json:"requireUploadSignature"` // "false" + RoutingOrder string `json:"routingOrder"` // 1 + SentDateTime string `json:"sentDateTime"` // 2023-05-26T18:55:48.257Z + Status string `json:"status"` // sent + UserId string `json:"userId"` // 9fd66d5d-7396-4b80-a85e-2a7e536471b1 +} + +type Sender struct { + AccountID string `json:"accountId"` // 9fd66d5d-7396-4b80-a85e-2a7e536471b1 + Email string `json:"email"` // test@test + IPAddress string `json:"ipAddress"` // 35.11.11.111 + UserName string `json:"userName"` // Test DocuSign + UserID string `json:"userId"` // 9fd66d5d-7396-4b80-a85e-2a7e536471b1 +} + +// DocuSignEnvelopeInformation is the root element +type DocuSignEnvelopeInformation struct { + XMLName xml.Name `xml:"DocuSignEnvelopeInformation"` + EnvelopeStatus EnvelopeStatus `xml:"EnvelopeStatus"` + // Additional fields can be added here if needed + FormData string `xml:"FormData"` +} + +// EnvelopeStatus represents the element +type EnvelopeStatus struct { + RecipientStatuses []RecipientStatus `xml:"RecipientStatuses>RecipientStatus"` + TimeGenerated string `xml:"TimeGenerated"` + EnvelopeID string `xml:"EnvelopeID"` + Subject string `xml:"Subject"` + UserName string `xml:"UserName"` + Email string `xml:"Email"` + Status string `xml:"Status"` + Created string `xml:"Created"` + Sent string `xml:"Sent"` + Delivered string `xml:"Delivered"` + Signed string `xml:"Signed"` + Completed string `xml:"Completed"` + ACStatus string `xml:"ACStatus"` + ACStatusDate string `xml:"ACStatusDate"` + ACHolder string `xml:"ACHolder"` + ACHolderEmail string `xml:"ACHolderEmail"` + ACHolderLocation string `xml:"ACHolderLocation"` + SigningLocation string `xml:"SigningLocation"` + SenderIPAddress string `xml:"SenderIPAddress"` + EnvelopePDFHash string `xml:"EnvelopePDFHash"` // Assuming string, adjust as necessary + CustomFields string `xml:"CustomFields"` // Assuming string, adjust as necessary + AutoNavigation bool `xml:"AutoNavigation"` + EnvelopeIdStamping bool `xml:"EnvelopeIdStamping"` + AuthoritativeCopy bool `xml:"AuthoritativeCopy"` + DocumentStatuses []DocumentStatus `xml:"DocumentStatuses>DocumentStatus"` + // Additional fields can be added here if needed +} + +// RecipientStatus represents the element +type RecipientStatus struct { + Type string `xml:"Type"` + Email string `xml:"Email"` + UserName string `xml:"UserName"` + RoutingOrder int `xml:"RoutingOrder"` + Sent string `xml:"Sent"` + Delivered string `xml:"Delivered"` + Signed string `xml:"Signed"` + DeclineReason string `xml:"DeclineReason"` + Status string `xml:"Status"` + RecipientIPAddress string `xml:"RecipientIPAddress"` + ClientUserId string `xml:"ClientUserId"` + CustomFields string `xml:"CustomFields"` + TabStatuses []TabStatus `xml:"TabStatuses>TabStatus"` + RecipientAttachment []Attachment `xml:"RecipientAttachment>Attachment"` + AccountStatus string `xml:"AccountStatus"` + EsignAgreementInformation EsignAgreement `xml:"EsignAgreementInformation"` + FormData FormData `xml:"FormData"` + RecipientId string `xml:"RecipientId"` + // Additional fields can be added here if needed +} + +// TabStatus represents the element +type TabStatus struct { + TabType string `xml:"TabType"` + Status string `xml:"Status"` + XPosition int `xml:"XPosition"` + YPosition int `xml:"YPosition"` + TabLabel string `xml:"TabLabel"` + TabName string `xml:"TabName"` + TabValue string `xml:"TabValue"` + DocumentID string `xml:"DocumentID"` + PageNumber int `xml:"PageNumber"` + CustomTabType string `xml:"CustomTabType"` + // Additional fields can be added here if needed +} + +// Attachment represents the element +type Attachment struct { + Data string `xml:"Data"` + Label string `xml:"Label"` + // Additional fields can be added here if needed +} + +// EsignAgreement represents the element +type EsignAgreement struct { + AccountEsignId string `xml:"AccountEsignId"` +} + +// FormData represents the element +type FormData struct { + XFDF XFDF `xml:"xfdf"` + // Additional fields can be added here if needed +} + +// XFDF represents the element within +type XFDF struct { + Fields []Field `xml:"fields>field"` + // Additional fields can be added here if needed +} + +// Field represents the element within +type Field struct { + Name string `xml:"name,attr"` + Value string `xml:"value"` + // Additional fields can be added here if needed +} + +// DocumentStatus represents the element +type DocumentStatus struct { + ID string `xml:"ID"` + Name string `xml:"Name"` + TemplateName string `xml:"TemplateName"` + Sequence int `xml:"Sequence"` + // Additional fields can be added here if needed +} diff --git a/cla-backend-go/v2/sign/service.go b/cla-backend-go/v2/sign/service.go index fa7384486..57ed40787 100644 --- a/cla-backend-go/v2/sign/service.go +++ b/cla-backend-go/v2/sign/service.go @@ -4,22 +4,42 @@ package sign import ( - "bytes" "context" - "encoding/json" + "encoding/base64" + "encoding/xml" "errors" "fmt" - "io/ioutil" + "io" + "math/rand" "net/http" + "net/url" + "strconv" "strings" + "time" + "github.com/communitybridge/easycla/cla-backend-go/emails" + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gerrits" + "github.com/communitybridge/easycla/cla-backend-go/github" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + "github.com/communitybridge/easycla/cla-backend-go/project/common" "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/cla_groups" + gitlab_activity "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab-activity" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + "github.com/communitybridge/easycla/cla-backend-go/v2/store" + "github.com/go-openapi/strfmt" + "github.com/gofrs/uuid" "github.com/sirupsen/logrus" acsService "github.com/communitybridge/easycla/cla-backend-go/v2/acs-service" "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" + sigs "github.com/communitybridge/easycla/cla-backend-go/gen/v1/restapi/operations/signatures" organizationService "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" projectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" @@ -28,14 +48,17 @@ import ( log "github.com/communitybridge/easycla/cla-backend-go/logging" "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" + gitlab_api "github.com/communitybridge/easycla/cla-backend-go/gitlab_api" "github.com/communitybridge/easycla/cla-backend-go/utils" ) // constants const ( DontLoadRepoDetails = false + DocSignFalse = "false" + DocusignCompleted = "Completed" ) // errors @@ -52,26 +75,74 @@ type ProjectRepo interface { // Service interface defines the sign service methods type Service interface { + VoidEnvelope(ctx context.Context, envelopeID, message string) error + PrepareSignRequest(ctx context.Context, signRequest *DocuSignEnvelopeRequest) (*DocusignEnvelopeResponse, error) + GetSignURL(email, recipientID, userName, clientUserId, envelopeID, returnURL string) (string, error) + createEnvelope(ctx context.Context, payload *DocuSignEnvelopeRequest) (string, error) + addDocumentToEnvelope(ctx context.Context, envelopeID, documentName string, document []byte) error + GetSignedDocument(ctx context.Context, envelopeID, documentID string) ([]byte, error) + GetEnvelopeDocuments(ctx context.Context, envelopeID string) ([]DocuSignDocument, error) + RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) + RequestIndividualSignature(ctx context.Context, input *models.IndividualSignatureInput, preferredEmail string) (*models.IndividualSignatureOutput, error) + RequestIndividualSignatureGerrit(ctx context.Context, input *models.IndividualSignatureInput) (*models.IndividualSignatureOutput, error) + SignedIndividualCallbackGithub(ctx context.Context, payload []byte, installationID, changeRequestID, repositoryID string) error + SignedIndividualCallbackGitlab(ctx context.Context, payload []byte, userID, organizationID, repositoryID, mergeRequestID string) error + SignedIndividualCallbackGerrit(ctx context.Context, payload []byte, userID string) error + SignedCorporateCallback(ctx context.Context, payload []byte, companyID, projectID string) error } // service type service struct { - ClaV1ApiURL string - companyRepo company.IRepository - projectRepo ProjectRepo - projectClaGroupsRepo projects_cla_groups.Repository - companyService company.IService + ClaV4ApiURL string + ClaV1ApiURL string + companyRepo company.IRepository + projectRepo ProjectRepo + projectClaGroupsRepo projects_cla_groups.Repository + companyService company.IService + claGroupService cla_groups.Service + docsignPrivateKey string + userService users.Service + signatureService signatures.SignatureService + storeRepository store.Repository + repositoryService repositories.Service + githubOrgService github_organizations.Service + gitlabOrgService gitlab_organizations.ServiceInterface + claLandingPage string + claLogoURL string + emailTemplateService emails.EmailTemplateService + eventsService events.Service + gitlabActivityService gitlab_activity.Service + gitlabApp *gitlab_api.App + gerritService gerrits.Service } // NewService returns an instance of v2 project service -func NewService(apiURL string, compRepo company.IRepository, projectRepo ProjectRepo, pcgRepo projects_cla_groups.Repository, compService company.IService) Service { +func NewService(apiURL, v1API string, compRepo company.IRepository, projectRepo ProjectRepo, pcgRepo projects_cla_groups.Repository, compService company.IService, claGroupService cla_groups.Service, docsignPrivateKey string, userService users.Service, signatureService signatures.SignatureService, storeRepository store.Repository, + repositoryService repositories.Service, githubOrgService github_organizations.Service, gitlabOrgService gitlab_organizations.ServiceInterface, claLandingPage string, claLogoURL string, emailTemplateService emails.EmailTemplateService, eventsService events.Service, gitlabActivityService gitlab_activity.Service, gitlabApp *gitlab_api.App, + gerritService gerrits.Service) Service { return &service{ - ClaV1ApiURL: apiURL, - companyRepo: compRepo, - projectRepo: projectRepo, - projectClaGroupsRepo: pcgRepo, - companyService: compService, + ClaV4ApiURL: apiURL, + ClaV1ApiURL: v1API, + companyRepo: compRepo, + projectRepo: projectRepo, + projectClaGroupsRepo: pcgRepo, + companyService: compService, + claGroupService: claGroupService, + docsignPrivateKey: docsignPrivateKey, + userService: userService, + signatureService: signatureService, + storeRepository: storeRepository, + githubOrgService: githubOrgService, + gitlabOrgService: gitlabOrgService, + repositoryService: repositoryService, + claLandingPage: claLandingPage, + claLogoURL: claLogoURL, + emailTemplateService: emailTemplateService, + gitlabActivityService: gitlabActivityService, + gitlabApp: gitlabApp, + gerritService: gerritService, + eventsService: eventsService, } } @@ -85,20 +156,6 @@ type requestCorporateSignatureInput struct { ReturnURL string `json:"return_url,omitempty"` } -type requestCorporateSignatureOutput struct { - ProjectID string `json:"project_id"` - CompanyID string `json:"company_id"` - SignatureID string `json:"signature_id"` - SignURL string `json:"sign_url"` -} - -func (in *requestCorporateSignatureOutput) toModel() *models.CorporateSignatureOutput { - return &models.CorporateSignatureOutput{ - SignURL: in.SignURL, - SignatureID: in.SignatureID, - } -} - func validateCorporateSignatureInput(input *models.CorporateSignatureInput) error { if input.SendAsEmail { log.Debugf("input.AuthorityName validation %s", input.AuthorityName) @@ -123,7 +180,7 @@ func validateCorporateSignatureInput(input *models.CorporateSignatureInput) erro return nil } -func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) { +func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) { // nolint f := logrus.Fields{ "functionName": "sign.RequestCorporateSignature", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), @@ -136,6 +193,12 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri "sendAsEmail": input.SendAsEmail, "returnURL": input.ReturnURL, } + + /** + 1. Ensure Company Exists + 2. Ensure this is a valid project + **/ + usc := userService.GetClient() log.WithFields(f).Debug("validating input parameters...") @@ -145,6 +208,7 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri return nil, err } + // 1. Ensure Company Exists var comp *v1Models.Company // Backwards compatible - if the signing entity name is not set, then we fall back to using the CompanySFID lookup // which will return the company record where the company name == signing entity name @@ -164,6 +228,7 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri } } + // 2. Ensure this is a valid project psc := projectService.GetClient() log.WithFields(f).Debug("looking up project by SFID...") project, err := psc.GetProject(utils.StringValue(input.ProjectSfid)) @@ -173,9 +238,9 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri } var claGroupID string - if project.Parent == "" || project.Parent == utils.TheLinuxFoundation { + if !utils.IsProjectHaveParent(project) || utils.IsProjectHasRootParent(project) || utils.GetProjectParentSFID(project) == "" { // this is root project - cgmlist, perr := s.projectClaGroupsRepo.GetProjectsIdsForFoundation(utils.StringValue(input.ProjectSfid)) + cgmlist, perr := s.projectClaGroupsRepo.GetProjectsIdsForFoundation(ctx, utils.StringValue(input.ProjectSfid)) if perr != nil { log.WithFields(f).WithError(err).Warn("unable to lookup other projects associated with this project SFID") return nil, perr @@ -186,16 +251,27 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri } claGroups := utils.NewStringSet() for _, cg := range cgmlist { - claGroups.Add(cg.ClaGroupID) + claGroup, claGroupErr := s.claGroupService.GetCLAGroup(ctx, cg.ClaGroupID) + if err != nil { + log.WithFields(f).WithError(claGroupErr).Warn("unable to lookup cla group") + return nil, err + } + + // ensure that cla group for project is a foundation level cla group + if claGroup != nil && cg.ProjectSFID == utils.StringValue(input.ProjectSfid) { + claGroups.Add(cg.ClaGroupID) + } } + if claGroups.Length() > 1 { // multiple cla group are linked with root_project // so we can not determine which cla-group to use return nil, errors.New("invalid project_sfid. multiple cla-groups are associated with this project_sfid") } claGroupID = (claGroups.List())[0] + } else { - cgm, perr := s.projectClaGroupsRepo.GetClaGroupIDForProject(utils.StringValue(input.ProjectSfid)) + cgm, perr := s.projectClaGroupsRepo.GetClaGroupIDForProject(ctx, utils.StringValue(input.ProjectSfid)) if perr != nil { log.WithFields(f).WithError(err).Warn("unable to lookup CLA Group ID for this project SFID") return nil, perr @@ -219,6 +295,21 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri return nil, ErrTemplateNotConfigured } + var currentUserEmail string + log.WithFields(f).Debugf("Loading user by username: %s...", lfUsername) + userModel, userErr := usc.GetUserByUsername(lfUsername) + if userErr != nil { + return nil, userErr + } + + if userModel != nil { + for _, email := range userModel.Emails { + if email != nil && *email.IsPrimary { + currentUserEmail = *email.EmailAddress + } + } + } + // Email flow if input.SendAsEmail { log.WithFields(f).Debugf("Sending request as an email to: %s...", input.AuthorityEmail.String()) @@ -232,21 +323,6 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri } } else { // Direct to DocuSign flow... - var currentUserEmail string - - log.WithFields(f).Debugf("Loading user by username: %s...", lfUsername) - userModel, userErr := usc.GetUserByUsername(lfUsername) - if userErr != nil { - return nil, userErr - } - - if userModel != nil { - for _, email := range userModel.Emails { - if email != nil && *email.IsPrimary { - currentUserEmail = *email.EmailAddress - } - } - } err = prepareUserForSigning(ctx, currentUserEmail, utils.StringValue(input.CompanySfid), utils.StringValue(input.ProjectSfid), input.SigningEntityName) if err != nil { @@ -254,11 +330,12 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri if _, ok := err.(*organizations.CreateOrgUsrRoleScopesConflict); !ok { return nil, err } + + log.WithFields(f).Debugf("User already has role assigned: %s", currentUserEmail) } } - log.WithFields(f).Debug("Forwarding request to v1 API for requestCorporateSignature...") - out, err := requestCorporateSignature(authorizationHeader, s.ClaV1ApiURL, &requestCorporateSignatureInput{ + signature, err := s.requestCorporateSignature(ctx, s.ClaV4ApiURL, &requestCorporateSignatureInput{ ProjectID: proj.ProjectID, CompanyID: comp.CompanyID, SigningEntityName: input.SigningEntityName, @@ -266,7 +343,8 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri AuthorityName: input.AuthorityName, AuthorityEmail: input.AuthorityEmail.String(), ReturnURL: input.ReturnURL.String(), - }) + }, comp, proj, lfUsername, currentUserEmail) + if err != nil { if input.AuthorityEmail.String() != "" { // remove role @@ -275,6 +353,7 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri log.WithFields(f).WithError(removeErr).Warnf("failed to remove signatory role. companySFID :%s, email :%s error: %+v", *input.CompanySfid, input.AuthorityEmail.String(), removeErr) } } + log.WithFields(f).WithError(err).Warnf("unable to request corporate signature") return nil, err } @@ -285,173 +364,2387 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri log.WithFields(f).WithError(companyACLError).Warnf("Unable to add user with LFID: %s to company ACL, companyID: %s", lfUsername, *input.CompanySfid) } - return out.toModel(), nil + log.WithFields(f).Debugf("Returning Signature: %+v", signature) + + return &models.CorporateSignatureOutput{ + SignURL: signature.SignatureSignURL, + SignatureID: signature.SignatureID, + }, nil +} + +func (s *service) getCorporateSignatureCallbackUrl(companyId, projectId string) string { + // s.ClaV4ApiURL = "https://cf2e-154-227-128-74.ngrok-free.app" //testing + return fmt.Sprintf("%s/v4/signed/corporate/%s/%s", s.ClaV4ApiURL, companyId, projectId) } -func requestCorporateSignature(authToken string, apiURL string, input *requestCorporateSignatureInput) (*requestCorporateSignatureOutput, error) { +func (s *service) SignedIndividualCallbackGithub(ctx context.Context, payload []byte, installationID, changeRequestID, repositoryID string) error { f := logrus.Fields{ - "functionName": "requestCorporateSignature", - "apiURL": apiURL, - "CompanyID": input.CompanyID, - "ProjectID": input.ProjectID, - "SigningEntityName": input.SigningEntityName, - "AuthorityName": input.AuthorityName, - "AuthorityEmail": input.AuthorityEmail, - "ReturnURL": input.ReturnURL, - "SendAsEmail": input.SendAsEmail, + "functionName": "sign.SignedIndividualCallbackGithub", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "installationID": installationID, + "changeRequestID": changeRequestID, + "repositoryID": repositoryID, } - log.WithFields(f).Debug("Processing request...") - requestBody, err := json.Marshal(input) + + log.WithFields(f).Debug("processing signed individual callback...") + + var info DocuSignEnvelopeInformation + + err := xml.Unmarshal(payload, &info) if err != nil { - log.WithFields(f).WithError(err).Warnf("problem marshalling input request - error: %+v", err) - return nil, err + log.WithFields(f).WithError(err).Warn("unable to unmarshal xml payload") + return err } - client := http.Client{} - log.WithFields(f).Debugf("requesting corporate signatures: %#v", string(requestBody)) - req, err := http.NewRequest("POST", apiURL+"/v1/request-corporate-signature", bytes.NewBuffer(requestBody)) + envelopeID := info.EnvelopeStatus.EnvelopeID + signatureID := info.EnvelopeStatus.RecipientStatuses[0].ClientUserId + status := info.EnvelopeStatus.RecipientStatuses[0].Status + signedDate := info.EnvelopeStatus.RecipientStatuses[0].Signed + documentID := info.EnvelopeStatus.DocumentStatuses[0].ID + fullName := fetchFullName(info) + + log.WithFields(f).Debugf("envelopeID: %s, signatureID: %s, status: %s, signedDate: %s, fullName: %s", envelopeID, signatureID, status, signedDate, fullName) + + _, currentTime := utils.CurrentTime() + + signature, err := s.signatureService.GetSignature(ctx, signatureID) if err != nil { - return nil, err + log.WithFields(f).WithError(err).Warn("unable to lookup signature by ID") + return err } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", authToken) - resp, err := client.Do(req) - if err != nil { - log.WithFields(f).Warnf("client request error: %+v", err) - return nil, err + + if signature == nil { + log.WithFields(f).WithError(err).Warn("unable to lookup signature by ID - signature not found") + return errors.New("unable to lookup signature by ID - signature not found") } - defer func() { - closeErr := resp.Body.Close() - if closeErr != nil { - log.WithFields(f).Warnf("error closing response body: %+v", closeErr) + + if status == DocusignCompleted { + log.WithFields(f).Debugf("envelope signed - status: %s", status) + updates := map[string]interface{}{ + "signature_signed": true, + "date_modified": currentTime, + "signed_on": currentTime, + "user_docusign_raw_xml": string(payload), + "user_docusign_name": fullName, + "user_docusign_date_signed": signedDate, + } + err = s.signatureService.UpdateSignature(ctx, signatureID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update signature record with envelope ID: %s", envelopeID) + return err } - }() - responseBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.WithFields(f).Warnf("error reading response body: %+v", err) - return nil, err - } - log.WithFields(f).Debugf("corporate signature response: %#v\n", string(responseBody)) - log.WithFields(f).Debugf("corporate signature response headers :%#v\n", resp.Header) - if strings.Contains(string(responseBody), "Company has already signed CCLA with this project") { - log.WithFields(f).Warnf("response contains error: %+v", responseBody) - return nil, errors.New("company has already signed CCLA with this project") - } else if strings.Contains(string(responseBody), "Contract Group does not support CCLAs.") { - log.WithFields(f).Warnf("response contains error: %+v", responseBody) - return nil, errors.New("contract Group does not support CCLAs") - } else if strings.Contains(string(responseBody), "user_error': 'user does not exist") { - log.WithFields(f).Warnf("response contains error: %+v", responseBody) - return nil, errors.New("user_error': 'user does not exist") - } else if strings.Contains(string(responseBody), "Internal server error") { - log.WithFields(f).Warnf("response contains error: %+v", responseBody) - return nil, errors.New("internal server error") - } + log.WithFields(f).Debugf("updated signature record: %s", signatureID) - var out requestCorporateSignatureOutput - err = json.Unmarshal(responseBody, &out) - if err != nil { - if _, ok := err.(*json.UnmarshalTypeError); ok { - return nil, errors.New(string(responseBody)) + // Update the repository provider with this change - this will update the comment (if necessary) + // and the status - do this early in the flow as the user will be immediately redirected back + installtionIDInt, err := strconv.Atoi(installationID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert installation ID to int: %s", installationID) + return err } - return nil, err - } - return &out, nil -} + repositoryIDInt, err := strconv.Atoi(repositoryID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert repository ID to int: %s", repositoryID) + return err + } -func removeSignatoryRole(ctx context.Context, userEmail string, companySFID string, projectSFID string) error { - f := logrus.Fields{"functionName": "removeSignatoryRole", "user_email": userEmail, "company_sfid": companySFID, "project_sfid": projectSFID} - log.WithFields(f).Debug("removing role for user") + changeRequestIDInt, err := strconv.Atoi(changeRequestID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert change request ID to int: %s", changeRequestID) + return err + } - usc := userService.GetClient() - // search user - log.WithFields(f).Debug("searching user by email") - user, err := usc.SearchUserByEmail(userEmail) - if err != nil { - log.WithFields(f).Debug("Failed to get user") - return err - } + log.WithFields(f).Debugf("updating change request for installation ID: %d, repository ID: %d, change request ID: %d", installtionIDInt, repositoryIDInt, changeRequestIDInt) + err = s.updateChangeRequest(ctx, int64(installtionIDInt), int64(repositoryIDInt), int64(changeRequestIDInt), signature.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update change request: %s", changeRequestID) + return err + } - log.WithFields(f).Debug("Getting role id") - acsClient := acsService.GetClient() - roleID, roleErr := acsClient.GetRoleID("cla-signatory") - if roleErr != nil { - log.WithFields(f).Debug("Failed to get role id for cla-signatory") - return roleErr - } - // Get scope id - log.WithFields(f).Debug("getting scope id") - orgClient := organizationService.GetClient() - scopeID, scopeErr := orgClient.GetScopeID(ctx, companySFID, projectSFID, "cla-signatory", "project|organization", user.Username) + claUser, userErr := s.userService.GetUser(signature.SignatureReferenceID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", signature.SignatureReferenceID) + return userErr + } - if scopeErr != nil { - log.WithFields(f).Debug("Failed to get scope id for cla-signatory role") - return scopeErr - } + if claUser.Username == "" { + if fullName != "" { + log.WithFields(f).Debugf("setting username for user with :%s", fullName) + updates := map[string]interface{}{ + "user_name": fullName, + } + log.WithFields(f).Debugf("updating user with username: %s", fullName) + _, err = s.userService.UpdateUser(signature.SignatureReferenceID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update user with username: %s", fullName) + return err + } + } + } - //Unassign role - log.WithFields(f).Debug("Unassigning role") - deleteErr := orgClient.DeleteOrgUserRoleOrgScopeProjectOrg(ctx, companySFID, roleID, scopeID, &user.Username, &userEmail) + // Remove the active signature + log.WithFields(f).Debugf("removing active signature metadata for user: %s", signature.SignatureReferenceID) + key := fmt.Sprintf("active_signature:%s", signature.SignatureReferenceID) + err = s.storeRepository.DeleteActiveSignatureMetaData(ctx, key) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to remove active signature metadata for user: %s", signature.SignatureReferenceID) + return err + } - if deleteErr != nil { - log.WithFields(f).Debug("Failed to remove cla-signatory role") - return deleteErr + //Get signed document + log.WithFields(f).Debugf("getting signed document for envelope ID: %s", envelopeID) + signedDocument, err := s.GetSignedDocument(ctx, envelopeID, documentID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signed document for envelope ID: %s", envelopeID) + return err + } + + // send email to user + log.WithFields(f).Debugf("sending email to user... ") + claGroup, err := s.claGroupService.GetCLAGroup(ctx, signature.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s", signature.ProjectID) + return err + } + + subject := fmt.Sprintf("EasyCLA: Individual CLA Signed for %s", claGroup.ProjectName) + pdfLink := fmt.Sprintf("%s/v3/signatures/%s/%s/icla/pdf", s.ClaV1ApiURL, signature.ProjectID, signature.SignatureReferenceID) + emailParams := emails.DocumentSignedTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: fullName, + }, + PdfLink: pdfLink, + ICLA: true, + } + email := utils.GetBestEmail(claUser) + if email == "" { + log.WithFields(f).Warnf("unable to find email for user: %+v", claUser) + return errors.New("unable to find email for user") + } + + recipients := []string{utils.GetBestEmail(claUser)} + + body, err := emails.RenderDocumentSignedTemplate(s.emailTemplateService, claGroup.Version, claGroup.ProjectExternalID, emailParams) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to render document signed template for project version: %s, project ID: %s", claGroup.Version, claGroup.ProjectID) + return err + } + + // send email to user + log.WithFields(f).Debugf("sending email to user... ") + err = utils.SendEmail(subject, body, recipients) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to send email to user: %s", claUser.Username) + return err + } + + log.WithFields(f).Debugf("email sent to user: %s", claUser.Username) + + if claUser.UserID == "" { + return fmt.Errorf("user id is empty for user: %s", claUser.Username) + } + + // store document on S3 + log.WithFields(f).Debugf("storing signed document on S3...") + err = utils.UploadToS3(signedDocument, signature.ProjectID, utils.ClaTypeICLA, claUser.UserID, signature.SignatureID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to store signed document on S3") + return err + } + + log.WithFields(f).Debugf("cla_group : %+v", claGroup) + + pcg, err := s.projectClaGroupsRepo.GetCLAGroup(ctx, signature.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project cla group by project ID: %s", signature.ProjectID) + return err + } + + log.WithFields(f).Debugf("project cla group: %+v", pcg) + projectName := claGroup.ProjectName + if projectName == "" { + projectName = pcg.ProjectName + log.WithFields(f).Debugf("project name not found in cla_group, using project cla group name: %s", projectName) + } + log.WithFields(f).Debugf("project name: %s", projectName) + + // Log the event + eventData := events.IndividualSignatureSignedEventData{ + ProjectName: projectName, + ProjectID: signature.ProjectID, + } + log.WithFields(f).Debugf("logging event: %+v", eventData) + eventArgs := &events.LogEventArgs{ + EventType: events.IndividualSignatureSigned, + ProjectID: signature.ProjectID, + UserID: claUser.UserID, + LfUsername: fullName, + EventData: &eventData, + CLAGroupID: signature.ProjectID, + } + log.WithFields(f).Debugf("logging event: %+v", eventArgs) + s.eventsService.LogEvent(eventArgs) + + } else { + log.WithFields(f).Debugf("envelope not signed - status: %s", status) } return nil } -func prepareUserForSigning(ctx context.Context, userEmail string, companySFID, projectSFID, signedEntityName string) error { +func fetchFullName(info DocuSignEnvelopeInformation) string { + var fullName string + for _, tabStatus := range info.EnvelopeStatus.RecipientStatuses[0].TabStatuses { + if tabStatus.TabLabel == "full_name" { + if tabStatus.TabValue != "" { + fullName = tabStatus.TabValue + } + } else if tabStatus.TabLabel == "signatory_name" { + if tabStatus.TabValue != "" { + fullName = tabStatus.TabValue + } + } + } + return fullName +} + +func (s *service) SignedIndividualCallbackGitlab(ctx context.Context, payload []byte, userID, organizationID, repositoryID, mergeRequestID string) error { f := logrus.Fields{ - "functionName": "sign.prepareUserForSigning", - utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "user_email": userEmail, - "company_sfid": companySFID, - "project_sfid": projectSFID, - "signedEntityName": signedEntityName, + "functionName": "sign.SignedIndividualCallbackGitlab", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "userID": userID, + "organizationID": organizationID, + "mergeRequestID": mergeRequestID, + "repositoryID": repositoryID, } - var ErrNotInOrg error - role := utils.CLASignatoryRole - log.WithFields(f).Debug("called") - usc := userService.GetClient() - // search user - log.WithFields(f).Debug("searching user by email") - user, err := usc.SearchUserByEmail(userEmail) + log.WithFields(f).Debug("processing signed individual callback...") + var info DocuSignEnvelopeInformation + + err := xml.Unmarshal(payload, &info) if err != nil { - log.WithFields(f).WithError(err).Debugf("User with email: %s does not have an LF login", userEmail) - return nil + log.WithFields(f).WithError(err).Warn("unable to unmarshal xml payload") + return err } - ac := acsService.GetClient() - log.WithFields(f).Debugf("getting role_id for %s", role) - roleID, err := ac.GetRoleID(role) + envelopeID := info.EnvelopeStatus.EnvelopeID + signatureID := info.EnvelopeStatus.RecipientStatuses[0].ClientUserId + status := info.EnvelopeStatus.RecipientStatuses[0].Status + signedDate := info.EnvelopeStatus.RecipientStatuses[0].Signed + documentID := info.EnvelopeStatus.DocumentStatuses[0].ID + fullName := fetchFullName(info) + + log.WithFields(f).Debugf("envelopeID: %s, signatureID: %s, status: %s, signedDate: %s, fullName: %s", envelopeID, signatureID, status, signedDate, fullName) + + _, currentTime := utils.CurrentTime() + + signature, err := s.signatureService.GetSignature(ctx, signatureID) if err != nil { - log.WithFields(f).WithError(err).Warnf("getting role_id for %s failed: %v", role, err.Error()) + log.WithFields(f).WithError(err).Warn("unable to lookup signature by ID") return err } - log.WithFields(f).Debugf("fetched role %s, role_id %s", role, roleID) - // assign user role of cla signatory for this project - osc := organizationService.GetClient() - // make user cla-signatory - log.WithFields(f).Debugf("assigning user role of %s...", role) - err = osc.CreateOrgUserRoleOrgScopeProjectOrg(ctx, userEmail, projectSFID, companySFID, roleID) - if err != nil { - if strings.Contains(err.Error(), "associated with some organization") { - msg := fmt.Sprintf("user: %s already associated with some organization", user.Username) - ErrNotInOrg = errors.New(msg) - log.WithFields(f).WithError(err).Warn(msg) - return ErrNotInOrg + if signature == nil { + log.WithFields(f).WithError(err).Warn("unable to lookup signature by ID - signature not found") + return errors.New("unable to lookup signature by ID - signature not found") + } + + if status == DocusignCompleted { + log.WithFields(f).Debugf("envelope signed - status: %s", status) + updates := map[string]interface{}{ + "signature_signed": true, + "date_modified": currentTime, + "signed_on": currentTime, + "user_docusign_raw_xml": string(payload), + "user_docusign_name": fullName, + "user_docusign_date_signed": signedDate, } - // Ignore conflict - role has already been assigned, otherwise, return the error - if _, ok := err.(*organizations.CreateOrgUsrRoleScopesConflict); !ok { - log.WithFields(f).WithError(err).Warnf("assigning user role of %s failed: %v", role, err) + err = s.signatureService.UpdateSignature(ctx, signatureID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update signature record with envelope ID: %s", envelopeID) return err } - } - return nil + log.WithFields(f).Debugf("updated signature record: %s", signatureID) + + gitlabOrg, err := s.gitlabOrgService.GetGitLabOrganizationByID(ctx, organizationID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup gitlab organization by ID: %s", organizationID) + return err + } + + repositoryIDInt, err := strconv.Atoi(repositoryID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert repository ID to int: %s", repositoryID) + return err + } + + mergeRequestIDInt, err := strconv.Atoi(mergeRequestID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert merge request ID to int: %s", mergeRequestID) + return err + } + + encryptedOauthResponse, err := s.gitlabOrgService.RefreshGitLabOrganizationAuth(ctx, gitlabOrg) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to refresh gitlab organization auth for organization ID: %s", organizationID) + return err + } + + gitlabClient, err := gitlab_api.NewGitlabOauthClient(*encryptedOauthResponse, s.gitlabApp) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to create gitlab client for organization ID: %s", organizationID) + return err + } + + log.WithFields(f).Debugf("fetching repository info for repository ID: %d", repositoryIDInt) + gitlabProject, err := gitlab_api.GetProjectByID(ctx, gitlabClient, repositoryIDInt) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup gitlab project by ID: %s", repositoryID) + return err + } + + log.WithFields(f).Debugf("fetching merge request info for merge request ID: %d", mergeRequestIDInt) + gitlabMr, err := gitlab_api.FetchMrInfo(gitlabClient, repositoryIDInt, mergeRequestIDInt) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to fetch merge request info for merge request ID: %s", mergeRequestID) + return err + } + + tokenPlaceHolder := "token" + input := gitlab_activity.ProcessMergeActivityInput{ + ProjectName: gitlabProject.Name, + ProjectID: gitlabProject.ID, + ProjectPath: gitlabProject.PathWithNamespace, + ProjectNamespace: gitlabProject.Namespace.Name, + MergeID: mergeRequestIDInt, + RepositoryPath: gitlabProject.PathWithNamespace, + LastCommitSha: gitlabMr.SHA, + } + + log.WithFields(f).Debugf("processing merge activity for input: %+v", input) + + err = s.gitlabActivityService.ProcessMergeActivity(ctx, tokenPlaceHolder, &input) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update change request: %s", mergeRequestID) + return err + } + + claUser, userErr := s.userService.GetUser(signature.SignatureReferenceID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", signature.SignatureReferenceID) + return userErr + } + + if claUser.Username == "" { + if fullName != "" { + log.WithFields(f).Debugf("setting username for user with :%s", fullName) + updates := map[string]interface{}{ + "user_name": fullName, + } + log.WithFields(f).Debugf("updating user with username: %s", fullName) + _, err = s.userService.UpdateUser(signature.SignatureReferenceID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update user with username: %s", fullName) + return err + } + } + } + + // Remove the active signature + log.WithFields(f).Debugf("removing active signature metadata for user: %s", signature.SignatureReferenceID) + key := fmt.Sprintf("active_signature:%s", signature.SignatureReferenceID) + err = s.storeRepository.DeleteActiveSignatureMetaData(ctx, key) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to remove active signature metadata for user: %s", signature.SignatureReferenceID) + return err + } + + //Get signed document + log.WithFields(f).Debugf("getting signed document for envelope ID: %s", envelopeID) + signedDocument, err := s.GetSignedDocument(ctx, envelopeID, documentID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signed document for envelope ID: %s", envelopeID) + return err + } + + // send email to user + log.WithFields(f).Debugf("sending email to user... ") + log.WithFields(f).Debugf("getting claGroupID: %s", signature.ProjectID) + claGroup, err := s.claGroupService.GetCLAGroup(ctx, signature.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s", signature.ProjectID) + return err + } + + subject := fmt.Sprintf("EasyCLA: Individual CLA Signed for %s", claGroup.ProjectName) + pdfLink := fmt.Sprintf("%s/v3/signatures/%s/%s/icla/pdf", s.ClaV1ApiURL, signature.ProjectID, signature.SignatureReferenceID) + emailParams := emails.DocumentSignedTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: fullName, + }, + PdfLink: pdfLink, + ICLA: true, + } + email := utils.GetBestEmail(claUser) + if email == "" { + log.WithFields(f).Warnf("unable to find email for user: %+v", claUser) + return errors.New("unable to find email for user") + } + + recipients := []string{utils.GetBestEmail(claUser)} + + body, err := emails.RenderDocumentSignedTemplate(s.emailTemplateService, claGroup.Version, claGroup.ProjectExternalID, emailParams) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to render document signed template for project version: %s, project ID: %s", claGroup.Version, claGroup.ProjectID) + return err + } + + // send email to user + log.WithFields(f).Debugf("sending email to user... ") + err = utils.SendEmail(subject, body, recipients) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to send email to user: %s", claUser.Username) + return err + } + + log.WithFields(f).Debugf("email sent to user: %s", claUser.Username) + + if claUser.UserID == "" { + return fmt.Errorf("user id is empty for user: %s", claUser.Username) + } + + // store document on S3 + log.WithFields(f).Debugf("storing signed document on S3...") + err = utils.UploadToS3(signedDocument, signature.ProjectID, utils.ClaTypeICLA, claUser.UserID, signature.SignatureID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to store signed document on S3") + return err + } + + // Log the event + log.WithFields(f).Debugf("logging event...") + s.eventsService.LogEvent(&events.LogEventArgs{ + EventType: events.IndividualSignatureSigned, + ProjectID: signature.ProjectID, + UserID: claUser.UserID, + EventData: &events.IndividualSignatureSignedEventData{ + ProjectName: claGroup.ProjectName, + Username: fullName, + ProjectID: signature.ProjectID, + }, + CLAGroupID: signature.ProjectID, + }) + } else { + log.WithFields(f).Debugf("envelope not signed - status: %s", status) + } + + return nil +} + +func (s *service) SignedIndividualCallbackGerrit(ctx context.Context, payload []byte, userID string) error { + f := logrus.Fields{ + "functionName": "sign.SignedIndividualCallbackGerrit", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "userID": userID, + } + + log.WithFields(f).Debug("processing signed individual callback...") + var info DocuSignEnvelopeInformation + + err := xml.Unmarshal(payload, &info) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to unmarshal xml payload") + return err + } + + envelopeID := info.EnvelopeStatus.EnvelopeID + signatureID := info.EnvelopeStatus.RecipientStatuses[0].ClientUserId + status := info.EnvelopeStatus.RecipientStatuses[0].Status + signedDate := info.EnvelopeStatus.RecipientStatuses[0].Signed + documentID := info.EnvelopeStatus.DocumentStatuses[0].ID + fullName := fetchFullName(info) + + log.WithFields(f).Debugf("envelopeID: %s, signatureID: %s, status: %s, signedDate: %s, fullName: %s", envelopeID, signatureID, status, signedDate, fullName) + + _, currentTime := utils.CurrentTime() + + signature, err := s.signatureService.GetSignature(ctx, signatureID) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to lookup signature by ID") + return err + } + + if signature == nil { + log.WithFields(f).WithError(err).Warn("unable to lookup signature by ID - signature not found") + return errors.New("unable to lookup signature by ID - signature not found") + } + + if status == DocusignCompleted { + log.WithFields(f).Debugf("envelope signed - status: %s", status) + updates := map[string]interface{}{ + "signature_signed": true, + "date_modified": currentTime, + "signed_on": currentTime, + "user_docusign_raw_xml": string(payload), + "user_docusign_name": fullName, + "user_docusign_date_signed": signedDate, + } + err = s.signatureService.UpdateSignature(ctx, signatureID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update signature record with envelope ID: %s", envelopeID) + return err + } + + log.WithFields(f).Debugf("updated signature record: %s", signatureID) + + claUser, userErr := s.userService.GetUser(signature.SignatureReferenceID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", signature.SignatureReferenceID) + return userErr + } + + if claUser.Username == "" { + if fullName != "" { + log.WithFields(f).Debugf("setting username for user with :%s", fullName) + updates := map[string]interface{}{ + "user_name": fullName, + } + log.WithFields(f).Debugf("updating user with username: %s", fullName) + _, err = s.userService.UpdateUser(signature.SignatureReferenceID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update user with username: %s", fullName) + return err + } + } + } + + //Get signed document + log.WithFields(f).Debugf("getting signed document for envelope ID: %s", envelopeID) + signedDocument, err := s.GetSignedDocument(ctx, envelopeID, documentID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signed document for envelope ID: %s", envelopeID) + return err + } + + log.WithFields(f).Debugf("getting claGroupID: %s", signature.ProjectID) + + claGroup, err := s.claGroupService.GetCLAGroup(ctx, signature.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s", signature.ProjectID) + return err + } + + log.WithFields(f).Debugf("claGroup: %+s found", claGroup.ProjectID) + + subject := fmt.Sprintf("EasyCLA: Individual CLA Signed for %s", claGroup.ProjectName) + pdfLink := fmt.Sprintf("%s/v3/signatures/%s/%s/icla/pdf", s.ClaV1ApiURL, signature.ProjectID, signature.SignatureReferenceID) + emailParams := emails.DocumentSignedTemplateParams{ + CommonEmailParams: emails.CommonEmailParams{ + RecipientName: fullName, + }, + PdfLink: pdfLink, + ICLA: true, + } + + email := utils.GetBestEmail(claUser) + if email == "" { + log.WithFields(f).Warnf("unable to find email for user: %+v", claUser) + return errors.New("unable to find email for user") + } + + recipients := []string{utils.GetBestEmail(claUser)} + + body, err := emails.RenderDocumentSignedTemplate(s.emailTemplateService, claGroup.Version, claGroup.ProjectExternalID, emailParams) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to render document signed template for project version: %s, project ID: %s", claGroup.Version, claGroup.ProjectID) + return err + } + + // send email to user + log.WithFields(f).Debugf("sending email to user... ") + err = utils.SendEmail(subject, body, recipients) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to send email to user: %s", claUser.Username) + return err + } + + log.WithFields(f).Debugf("email sent to user: %s", claUser.Username) + + if claUser.UserID == "" { + return fmt.Errorf("user id is empty for user: %s", claUser.Username) + } + + // store document on S3 + log.WithFields(f).Debugf("storing signed document on S3...") + err = utils.UploadToS3(signedDocument, signature.ProjectID, utils.ClaTypeICLA, claUser.UserID, signature.SignatureID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to store signed document on S3") + return err + } + + // event data + eventData := &events.IndividualSignatureSignedEventData{ + Username: fullName, + ProjectID: signature.ProjectID, + ProjectName: claGroup.ProjectName, + } + + // Log the event + log.WithFields(f).Debugf("logging event...") + s.eventsService.LogEvent(&events.LogEventArgs{ + EventType: events.IndividualSignatureSigned, + ProjectID: signature.ProjectID, + UserID: claUser.UserID, + EventData: eventData, + CLAGroupID: signature.ProjectID, + }) + + // // Add User to Gerrit Group + // if claUser.LfUsername != "" { + // log.WithFields(f).Debugf("adding user to gerrit group: %s", claUser.LfUsername) + // err = s.gerritService.AddUserToGroup(ctx, nil, signature.ProjectID, claUser.LfUsername, utils.ClaTypeICLA) + // if err != nil { + // log.WithFields(f).WithError(err).Warnf("unable to add user to gerrit group") + // return err + // } + // } else { + // log.WithFields(f).Warnf("user LF username is empty") + // } + + } else { + log.WithFields(f).Debugf("envelope not signed - status: %s", status) + } + + return nil +} + +func (s *service) SignedCorporateCallback(ctx context.Context, payload []byte, companyID, projectID string) error { + f := logrus.Fields{ + "functionName": "sign.SignedCorporateCallback", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "companyID": companyID, + "projectID": projectID, + } + + log.WithFields(f).Debug("processing signed corporate callback...") + var info DocuSignEnvelopeInformation + + err := xml.Unmarshal(payload, &info) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to unmarshal xml payload") + return err + } + + envelopeID := info.EnvelopeStatus.EnvelopeID + + log.WithFields(f).Debugf("envelopeID: %s", envelopeID) + + // Get the CLA Group + log.WithFields(f).Debugf("loading CLA Group by ID: %s", projectID) + claGroup, err := s.claGroupService.GetCLAGroup(ctx, projectID) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s", projectID) + return err + } + + if claGroup == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s - not found", projectID) + return errors.New("unable to lookup CLA Group by ID - not found") + } + + // Get the company + log.WithFields(f).Debugf("loading company by ID: %s", companyID) + companyModel, err := s.companyRepo.GetCompany(ctx, companyID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup company by ID: %s", companyID) + return err + } + + // Assumme only one signature per company/project + var signatureID string + var signature *v1Models.Signature + clientUserID := info.EnvelopeStatus.RecipientStatuses[0].ClientUserId + if clientUserID == "" { + approved := true + var sigErr error + signature, sigErr = s.signatureService.GetCorporateSignature(ctx, projectID, companyID, &approved, nil) + // signatures, sigErr := s.signatureService.GetCorporateSignatures(ctx, companyID, projectID, &approved, nil) + if sigErr != nil { + log.WithFields(f).WithError(sigErr).Warnf("unable to lookup corporate signatures by company ID: %s, project ID: %s", companyID, projectID) + return sigErr + } + // if len(signatures) == 0 { + // log.WithFields(f).WithError(err).Warnf("unable to lookup corporate signatures by company ID: %s, project ID: %s - not found", companyID, projectID) + // return errors.New("unable to lookup corporate signatures by company ID - not found") + // } + // signature = getLatestSignature(signatures) + log.WithFields(f).Debugf("signature: %+v", signature) + signatureID = signature.SignatureID + } else { + signatureID = clientUserID + signature, err = s.signatureService.GetSignature(ctx, signatureID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup signature by ID: %s", signatureID) + return err + } + if signature == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup signature by ID: %s - not found", signatureID) + return errors.New("unable to lookup signature by ID - not found") + } + } + + log.WithFields(f).Debugf("signatureID: %s", signatureID) + var user *v1Models.User + if signature.SignatureReferenceType == utils.SignatureReferenceTypeUser { + log.WithFields(f).Debugf("looking up user by ID: %s", signature.SignatureReferenceID) + user, err = s.userService.GetUser(signature.SignatureReferenceID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by ID: %s", signature.SignatureReferenceID) + return err + } + if user == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by ID: %s - not found", signature.SignatureReferenceID) + return errors.New("unable to lookup user by ID - not found") + } + } else if signature.SignatureReferenceType == utils.SignatureReferenceTypeCompany { + claManagerList := signature.SignatureACL + if len(claManagerList) > 0 { + log.WithFields(f).Debugf("looking up user by LF Username: %s", claManagerList[0].LfUsername) + user, err = s.userService.GetUserByLFUserName(claManagerList[0].LfUsername) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by LF Username: %s", claManagerList[0].LfUsername) + return err + } + if user == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by LFUsername: %s - not found", claManagerList[0].LfUsername) + return errors.New("unable to lookup user by ID - not found") + } + + log.WithFields(f).Debugf("found cla manager: %+v", user) + } + } + + // Update the signature status if changed + status := info.EnvelopeStatus.Status + if status == DocusignCompleted && !signature.SignatureSigned { + _, currentTime := utils.CurrentTime() + updates := map[string]interface{}{ + "signature_signed": true, + "date_modified": currentTime, + "signed_on": currentTime, + } + + userSignedDate := info.EnvelopeStatus.RecipientStatuses[0].Signed + if userSignedDate != "" { + updates["user_docusign_date_signed"] = userSignedDate + } + + updates["user_docusign_raw_xml"] = string(payload) + + // Update the signature record + log.WithFields(f).Debugf("updating signature record: %s", signatureID) + err = s.signatureService.UpdateSignature(ctx, signatureID, updates) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to update signature record: %s", signatureID) + return err + } + + log.WithFields(f).Debugf("updated signature record: %s", signatureID) + } + + // store document on S3 + log.WithFields(f).Debugf("storing signed document on S3...") + signedDocument, err := s.GetSignedDocument(ctx, envelopeID, info.EnvelopeStatus.DocumentStatuses[0].ID) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signed document for envelope ID: %s", envelopeID) + return err + } + + err = utils.UploadToS3(signedDocument, projectID, utils.ClaTypeCCLA, companyID, signatureID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to store signed document on S3") + return err + } + + log.WithFields(f).Debugf("signed document stored on S3") + + // Log the event + log.WithFields(f).Debugf("logging event...") + s.eventsService.LogEvent(&events.LogEventArgs{ + EventType: events.CorporateSignatureSigned, + ProjectName: claGroup.ProjectName, + EventData: &events.CorporateSignatureSignedEventData{ + ProjectName: claGroup.ProjectName, + CompanyName: companyModel.CompanyName, + SignatoryName: getUserName(user), + }, + CLAGroupID: projectID, + UserID: user.UserID, + CompanyID: companyID, + CompanySFID: companyModel.CompanyExternalID, + }) + + // // Check if project is a gerrit instance + // var gerrits []*v1Models.Gerrit + // gerritList, err := s.gerritService.GetClaGroupGerrits(ctx, projectID) + // if err != nil { + // log.WithFields(f).WithError(err).Warnf("unable to get gerrit instances for project: %s", projectID) + // gerrits = []*v1Models.Gerrit{} + // } else { + // log.WithFields(f).Debugf("gerrit instances found for project: %s", projectID) + // gerrits = gerritList.List + // } + + // // Add User to Gerrit Group + // if len(gerrits) > 0 { + // if user.LfUsername != "" { + // log.WithFields(f).Debugf("adding user to gerrit group: %s", user.LfUsername) + // err = s.gerritService.AddUserToGroup(ctx, nil, projectID, user.LfUsername, utils.ClaTypeCCLA) + // if err != nil { + // log.WithFields(f).WithError(err).Warnf("unable to add user to gerrit group") + // return err + // } + // } else { + // log.WithFields(f).Warnf("user LF username is empty") + // } + // } + + return nil + +} + +func (s *service) RequestIndividualSignature(ctx context.Context, input *models.IndividualSignatureInput, preferredEmail string) (*models.IndividualSignatureOutput, error) { + f := logrus.Fields{ + "functionName": "sign.RequestIndividualSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectID": *input.ProjectID, + "returnURL": input.ReturnURL, + "returnURLType": input.ReturnURLType, + "userID": *input.UserID, + } + + /** + 1. Ensure this is a valid user + 2. Ensure this is a valid project + 3. Check for active signature object with this project. If the user has signed the most recent version they should not be able to sign again. + 4. Generate signature callback url + 5. Get signature return URL + 6. Get latest document + 7. if the CCLA/ICLA template is missing we wont have a document and return an error + 8. Create new signature object + 9. Set signature ACL + 10. Populate sign url + 11. Save signature + **/ + + // 1. Ensure this is a valid user + log.WithFields(f).Debugf("looking up user by ID: %s", *input.UserID) + user, err := s.userService.GetUser(*input.UserID) + if err != nil || user == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by ID: %s", *input.UserID) + return nil, err + } + + // 2. Ensure this is a valid project + log.WithFields(f).Debugf("looking up project by ID: %s", *input.ProjectID) + claGroup, err := s.claGroupService.GetCLAGroup(ctx, *input.ProjectID) + if err != nil || claGroup == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project by ID: %s", *input.ProjectID) + return nil, err + } + + // 3. Check for active signature object with this project. If the user has signed the most recent version they should not be able to sign again. + log.WithFields(f).Debugf("checking for active signature object with this project...") + + sigParams := sigs.GetUserSignaturesParams{ + UserID: *input.UserID, + UserName: &user.Username, + } + userSignatures, err := s.signatureService.GetUserSignatures(ctx, sigParams, input.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user signatures by user ID: %s", *input.UserID) + return nil, err + } + log.WithFields(f).Debugf("found %d signatures for user: %s", len(userSignatures.Signatures), *input.UserID) + latestSignature := getLatestSignature(userSignatures.Signatures) + + // loading latest document + log.WithFields(f).Debugf("loading latest individual document for project: %s", *input.ProjectID) + latestDocument, err := common.GetCurrentDocument(ctx, claGroup.ProjectIndividualDocuments) + + log.WithFields(f).Debugf("latest document discovered: %+v", latestDocument) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup latest individual document for project: %s", *input.ProjectID) + return nil, err + } + + if common.AreClaGroupDocumentsEqual(latestDocument, v1Models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warnf("unable to lookup latest individual document for project: %s", *input.ProjectID) + return nil, errors.New("unable to lookup latest individual document for project") + } + + // creating individual default values + log.WithFields(f).Debugf("creating individual default values...") + defaultValues := s.createDefaultIndividualValues(user, preferredEmail) + log.WithFields(f).Debugf("default values: %+v", defaultValues) + + // 4. Generate signature callback url + log.WithFields(f).Debugf("generating signature callback url...") + activeSignatureMetadata, err := s.storeRepository.GetActiveSignatureMetaData(ctx, *input.UserID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature meta data for user: %s", *input.UserID) + return nil, err + } + + log.WithFields(f).Debugf("active signature metadata: %+v", activeSignatureMetadata) + + log.WithFields(f).Debugf("generating signature callback url...") + var callBackURL string + + if strings.ToLower(input.ReturnURLType) == utils.GitHubType { + callBackURL, err = s.getIndividualSignatureCallbackURL(ctx, *input.UserID, activeSignatureMetadata) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signature callback url for user: %s", *input.UserID) + return nil, err + } + + } else if strings.ToLower(input.ReturnURLType) == utils.GitLabLower { + callBackURL, err = s.getIndividualSignatureCallbackURLGitlab(ctx, *input.UserID, activeSignatureMetadata) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signature callback url for user: %s", *input.UserID) + return nil, err + } + } + + log.WithFields(f).Debugf("signature callback url: %s", callBackURL) + + var acl string + if strings.ToLower(input.ReturnURLType) == utils.GitHubType { + acl = fmt.Sprintf("%s:%s", strings.ToLower(input.ReturnURLType), user.GithubID) + } else if strings.ToLower(input.ReturnURLType) == "gitlab" { + acl = fmt.Sprintf("%s:%s", strings.ToLower(input.ReturnURLType), user.GitlabID) + } + + log.WithFields(f).Debugf("acl: %s", acl) + + majorVersion, err := strconv.Atoi(latestDocument.DocumentMajorVersion) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document major version to int: %s", latestDocument.DocumentMajorVersion) + return nil, err + } + + minorVersion, err := strconv.Atoi(latestDocument.DocumentMinorVersion) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document minor version to int: %s", latestDocument.DocumentMinorVersion) + return nil, err + } + + if latestSignature != nil { + log.WithFields(f).Debugf("comparing latest signature document version: %s to latest document version: %s", latestSignature.SignatureDocumentMajorVersion, latestDocument.DocumentMajorVersion) + if latestDocument.DocumentMajorVersion == latestSignature.SignatureDocumentMajorVersion { + + log.WithFields(f).Warnf("user: already has a signature with this project: %s", *input.ProjectID) + + // Regenerate and set the signing URL - This will update the signature record + log.WithFields(f).Debugf("regenerating signing URL for user: %s", *input.UserID) + _, currentTime := utils.CurrentTime() + itemSignature := signatures.ItemSignature{ + SignatureID: latestSignature.SignatureID, + DateModified: currentTime, + SignatureReferenceType: latestSignature.SignatureReferenceType, + SignatureEnvelopeID: latestSignature.SignatureEnvelopeID, + SignatureType: latestSignature.SignatureType, + SignatureReferenceID: latestSignature.SignatureReferenceID, + SignatureProjectID: latestSignature.ProjectID, + SignatureApproved: latestSignature.SignatureApproved, + SignatureSigned: latestSignature.SignatureSigned, + SignatureReferenceName: latestSignature.SignatureReferenceName, + SignatureReferenceNameLower: latestSignature.SignatureReferenceNameLower, + SignedOn: latestSignature.SignedOn, + SignatureReturnURL: string(input.ReturnURL), + SignatureReturnURLType: input.ReturnURLType, + SignatureCallbackURL: callBackURL, + SignatureACL: []string{acl}, + SignatureDocumentMajorVersion: majorVersion, + SignatureDocumentMinorVersion: minorVersion, + } + signErr := s.populateSignURL(ctx, &itemSignature, callBackURL, "", "", false, "", "", defaultValues, preferredEmail) + if signErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url for user: %s", *input.UserID) + return nil, signErr + } + + return &models.IndividualSignatureOutput{ + SignURL: itemSignature.SignatureSignURL, + SignatureID: latestSignature.SignatureID, + UserID: latestSignature.SignatureReferenceID, + ProjectID: *input.ProjectID, + }, nil + } else { + log.WithFields(f).Debugf("user does NOT have a signature with this project : %s", *input.ProjectID) + } + + } + + // 5. Get signature return URL + log.WithFields(f).Debugf("getting signature return url...") + var returnURL string + if input.ReturnURL.String() == "" { + log.WithFields(f).Warnf("signature return url is empty") + returnURL, err = getActiveSignatureReturnURL(*input.UserID, activeSignatureMetadata) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature return url for user: %s", *input.UserID) + return nil, err + } + if returnURL == "" { + log.WithFields(f).Warnf("signature return url is empty") + return &models.IndividualSignatureOutput{ + UserID: *input.UserID, + ProjectID: *input.ProjectID, + }, nil + } + } + + // 6. Get latest document + log.WithFields(f).Debugf("getting latest document...") + document, err := common.GetCurrentDocument(ctx, claGroup.ProjectIndividualDocuments) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get latest document for project: %s", *input.ProjectID) + return nil, err + } + + // 7. if the CCLA/ICLA template is missing we wont have a document and return an error + if common.AreClaGroupDocumentsEqual(document, v1Models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warnf("unable to get latest document for project: %s", *input.ProjectID) + return nil, errors.New("unable to get latest document for project") + } + + // 8. Create new signature object + log.WithFields(f).Debugf("creating new signature object...") + signatureID := uuid.Must(uuid.NewV4()).String() + _, currentTime := utils.CurrentTime() + + itemSignature := signatures.ItemSignature{ + SignatureID: signatureID, + DateCreated: currentTime, + DateModified: currentTime, + SignatureSigned: false, + SignatureApproved: true, + SignatureDocumentMajorVersion: majorVersion, + SignatureDocumentMinorVersion: minorVersion, + SignatureReferenceID: *input.UserID, + SignatureReferenceName: getUserName(user), + SignatureType: utils.SignatureTypeCLA, + SignatureReturnURLType: input.ReturnURLType, + SignatureProjectID: *input.ProjectID, + SignatureReturnURL: input.ReturnURL.String(), + SignatureCallbackURL: callBackURL, + SignatureReferenceType: "user", + SignatureACL: []string{acl}, + SignatureReferenceNameLower: strings.ToLower(getUserName(user)), + } + + // 10. Populate sign url + log.WithFields(f).Debugf("populating sign url...") + err = s.populateSignURL(ctx, &itemSignature, callBackURL, "", "", false, "", "", defaultValues, preferredEmail) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url for user: %s", *input.UserID) + return nil, err + } + + log.WithFields(f).Debugf("Updated signature: %+v", itemSignature) + + return &models.IndividualSignatureOutput{ + UserID: itemSignature.SignatureReferenceID, + ProjectID: itemSignature.SignatureProjectID, + SignatureID: itemSignature.SignatureID, + SignURL: itemSignature.SignatureSignURL, + }, nil +} + +func getUserName(user *v1Models.User) string { + + if user.Username != "" { + return user.Username + } + if user.LfUsername != "" { + return user.LfUsername + } + + if user.GithubUsername != "" { + return user.GithubUsername + } + if user.GitlabUsername != "" { + return user.GitlabUsername + } + return "" +} + +func (s *service) getIndividualSignatureCallbackURLGitlab(ctx context.Context, userID string, metadata map[string]interface{}) (string, error) { + f := logrus.Fields{ + "functionName": "sign.getIndividualSignatureCallbackURLGitlab", + "userID": userID, + } + + log.WithFields(f).Debugf("generating signature callback url...") + var err error + var repositoryID string + var mergeRequestID string + + if metadata == nil { + metadata, err = s.storeRepository.GetActiveSignatureMetaData(ctx, userID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature meta data for user: %s", userID) + return "", err + } + } + + if found, ok := metadata["repository_id"].(string); ok { + repositoryID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get repository ID for user: %s", userID) + return "", err + } + + if found, ok := metadata["merge_request_id"].(string); ok { + mergeRequestID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get pull request ID for user: %s", userID) + return "", err + } + + // Get repository + log.WithFields(f).Debugf("getting repository by external ID: %s", repositoryID) + gitlabRepo, err := s.repositoryService.GetRepositoryByExternalID(ctx, repositoryID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get organization ID for repository ID: %s", repositoryID) + return "", err + } + + log.WithFields(f).Debugf("searching for gitlab organization by name: %s", gitlabRepo.RepositoryOrganizationName) + gitlabOrg, err := s.gitlabOrgService.GetGitLabOrganizationByName(ctx, gitlabRepo.RepositoryOrganizationName) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get organization ID for repository ID: %s", repositoryID) + return "", err + } + + if gitlabOrg.OrganizationID == "" { + log.WithFields(f).WithError(err).Warnf("unable to get organization ID for repository ID: %s", repositoryID) + return "", err + } + + // s.ClaV4ApiURL = "https://a970-102-217-56-29.ngrok-free.app" + return fmt.Sprintf("%s/v4/signed/gitlab/individual/%s/%s/%s/%s", s.ClaV4ApiURL, userID, gitlabOrg.OrganizationID, repositoryID, mergeRequestID), nil + +} + +func (s *service) getIndividualSignatureCallbackURL(ctx context.Context, userID string, metadata map[string]interface{}) (string, error) { + f := logrus.Fields{ + "functionName": "sign.getIndividualSignatureCallbackURL", + "userID": userID, + } + + log.WithFields(f).Debugf("generating signature callback url...") + var err error + var installationId int64 + var repositoryID string + var pullRequestID string + + if metadata == nil { + metadata, err = s.storeRepository.GetActiveSignatureMetaData(ctx, userID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature meta data for user: %s", userID) + return "", err + } + } + + if found, ok := metadata["repository_id"].(string); ok { + repositoryID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get repository ID for user: %s", userID) + return "", err + } + + log.WithFields(f).Debugf("found repository ID: %s", repositoryID) + + if found, ok := metadata["pull_request_id"].(string); ok { + pullRequestID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get pull request ID for user: %s", userID) + return "", err + } + + log.WithFields(f).Debugf("found pull request ID: %s", pullRequestID) + + // Get installation ID through a helper function + log.WithFields(f).Debugf("getting repository...") + githubRepository, err := s.repositoryService.GetRepositoryByExternalID(ctx, repositoryID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get installation ID for repository ID: %s", repositoryID) + return "", err + } + + // Get github organization + log.WithFields(f).Debugf("getting github organization...") + githubOrg, err := s.githubOrgService.GetGitHubOrganizationByName(ctx, githubRepository.RepositoryOrganizationName) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get github organization for repository ID: %s", repositoryID) + return "", err + } + + installationId = githubOrg.OrganizationInstallationID + if installationId == 0 { + log.WithFields(f).WithError(err).Warnf("unable to get installation ID for repository ID: %s", repositoryID) + return "", err + } + + callbackURL := fmt.Sprintf("%s/v4/signed/individual/%d/%s/%s", s.ClaV4ApiURL, installationId, repositoryID, pullRequestID) + return callbackURL, nil +} + +//nolint:gocyclo +func (s *service) populateSignURL(ctx context.Context, + latestSignature *signatures.ItemSignature, callbackURL string, + authorityOrSignatoryName, authorityOrSignatoryEmail string, + sendAsEmail bool, + claManagerName, claManagerEmail string, + defaultValues map[string]interface{}, preferredEmail string) error { + + f := logrus.Fields{ + "functionName": "sign.populateSignURL", + "authorityOrSignatoryName": authorityOrSignatoryName, + "authorityOrSignatoryEmail": authorityOrSignatoryEmail, + "preferredEmail": preferredEmail, + "callbackURL": callbackURL, + } + log.WithFields(f).Debugf("populating sign url...") + signatureReferenceType := latestSignature.SignatureReferenceType + + log.WithFields(f).Debugf("signatureReferenceType: %s", signatureReferenceType) + log.WithFields(f).Debugf("processing signing request...") + log.WithFields(f).Debugf("latestSignature: %+v", latestSignature) + + var userSignatureName string + var userSignatureEmail string + var document v1Models.ClaGroupDocument + var project *v1Models.ClaGroup + var companyModel *v1Models.Company + var err error + var signer DocuSignRecipient + var emailBody string + var emailSubject string + + // populate user details + userDetails, err := s.populateUserDetails(ctx, signatureReferenceType, latestSignature, claManagerName, claManagerEmail, sendAsEmail, preferredEmail) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate user details for signatureReferenceType: %s", signatureReferenceType) + return err + } + + userSignatureName = userDetails.userSignatureName + userSignatureEmail = userDetails.userSignatureEmail + + log.WithFields(f).Debugf("userSignatureName: %s, userSignatureEmail: %s", userSignatureName, userSignatureEmail) + + // Get the document template to sign + log.WithFields(f).Debugf("getting document template to sign...") + project, err = s.projectRepo.GetCLAGroupByID(ctx, latestSignature.SignatureProjectID, DontLoadRepoDetails) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project by ID: %s", latestSignature.SignatureProjectID) + return err + } + + if project == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project by ID: %s", latestSignature.SignatureProjectID) + return errors.New("no project lookup error") + } + + if signatureReferenceType == utils.SignatureReferenceTypeCompany { + log.WithFields(f).Debugf("loading project corporate document...") + document, err = common.GetCurrentDocument(ctx, project.ProjectCorporateDocuments) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project corporate document for project: %s", latestSignature.SignatureProjectID) + return err + } + } else { + log.WithFields(f).Debugf("loading project individual document...") + document, err = common.GetCurrentDocument(ctx, project.ProjectIndividualDocuments) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project individual document for project: %s", latestSignature.SignatureProjectID) + return err + } + } + + // Void the existing envelope to prevent multiple envelopes pending for a signer + envelopeID := latestSignature.SignatureEnvelopeID + if envelopeID != "" { + message := fmt.Sprintf("You are getting this message because your DocuSign Session for project %s expired. A new session will be in place for your signing process.", project.ProjectName) + log.WithFields(f).Debug(message) + err = s.VoidEnvelope(ctx, envelopeID, message) + if err != nil { + log.WithFields(f).WithError(err).Warnf("DocuSign error while voiding the envelope - regardless, continuing on..., error: %s", err) + } + } + + // create a new source and rand object + src := rand.NewSource(time.Now().UnixNano()) + r := rand.New(src) //nolint:gosec + + randomInteger := r.Intn(1000000) //nolint:gosec + documentID := strconv.Itoa(randomInteger) + + tab := getTabsFromDocument(&document, documentID, defaultValues) + + // # Create the envelope request object + + if sendAsEmail { + log.WithFields(f).Warnf("assigning signatory name/email: %s/%s", authorityOrSignatoryName, authorityOrSignatoryEmail) + signatoryEmail := authorityOrSignatoryEmail + signatoryName := authorityOrSignatoryName + + var projectName string + var companyName string + + if project != nil { + projectName = project.ProjectName + } + + if companyModel != nil { + companyName = companyModel.CompanyName + } + + pcgs, pcgErr := s.projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, project.ProjectID) + if pcgErr != nil { + log.WithFields(f).Debugf("problem fetching project cla groups by id :%s, err: %+v", project.ProjectID, pcgErr) + return pcgErr + } + + if len(pcgs) == 0 { + log.WithFields(f).Debugf("no project cla groups found for project id :%s", project.ProjectID) + return errors.New("no project cla groups found for project id") + } + + var projectNames []string + for _, pcg := range pcgs { + projectNames = append(projectNames, pcg.ProjectName) + } + + if len(projectNames) == 0 { + projectNames = []string{projectName} + } + + claSignatoryParams := &ClaSignatoryEmailParams{ + ClaGroupName: project.ProjectName, + SignatoryName: signatoryName, + CompanyName: companyName, + ProjectNames: projectNames, + ProjectVersion: project.Version, + ClaManagerName: claManagerName, + ClaManagerEmail: claManagerEmail, + } + + log.WithFields(f).Debugf("claSignatoryParams: %+v", claSignatoryParams) + emailSubject, emailBody = claSignatoryEmailContent(*claSignatoryParams) + log.WithFields(f).Debugf("subject: %s, body: %s", emailSubject, emailBody) + + signer = DocuSignRecipient{ + Email: signatoryEmail, + Name: signatoryName, + Tabs: tab, + RecipientId: "1", + RoleName: "signer", + } + + } else { + // This will be the Initial CLA Manager + signatoryName := userSignatureName + signatoryEmail := userSignatureEmail + + // Assigning a clientUserId does not send an email. + // It assumes that the user handles the communication with the client. + // In this case, the user opened the docusign document to manually sign it. + // Thus the email does not need to be sent. + + log.WithFields(f).Debugf("signatoryName: %s, signatoryEmail: %s", signatoryName, signatoryEmail) + + // # Max length for emailSubject is 100 characters - guard/truncate if necessary + emailSubject = fmt.Sprintf("EasyCLA: CLA Signature Request for %s", project.ProjectName) + if len(emailSubject) > 100 { + emailSubject = emailSubject[:97] + "..." + } + + // # Update Signed for label according to signature_type (company or name) + var userIdentifier string + if signatureReferenceType == utils.SignatureReferenceTypeCompany && companyModel != nil { + userIdentifier = companyModel.CompanyName + } else { + if signatoryName == "Unknown" || signatoryName == "" { + userIdentifier = signatoryEmail + } else { + userIdentifier = signatoryName + } + } + + log.WithFields(f).Debugf("userIdentifier: %s", userIdentifier) + + emailBody = fmt.Sprintf("CLA Sign Request for %s", userIdentifier) + + signer = DocuSignRecipient{ + Email: signatoryEmail, + Name: signatoryName, + Tabs: tab, + RecipientId: "1", + ClientUserId: latestSignature.SignatureID, + RoleName: "signer", + } + } + + contentType := document.DocumentContentType + var pdf []byte + + if document.DocumentS3URL != "" { + log.WithFields(f).Debugf("getting document resource from s3: %s...", document.DocumentS3URL) + pdf, err = s.getDocumentResource(document.DocumentS3URL) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get document resource from s3 for document: %s", document.DocumentS3URL) + return err + } + } else if strings.HasPrefix(contentType, "url+") { + log.WithFields(f).Debugf("getting document resource from url...") + pdfURL := document.DocumentContent + pdf, err = s.getDocumentResource(pdfURL) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get document resource from url: %s", pdfURL) + return err + } + } else { + log.WithFields(f).Debugf("getting document resource from content...") + content := document.DocumentContent + pdf = []byte(content) + } + + documentName := document.DocumentName + log.WithFields(f).Debugf("documentName: %s", documentName) + log.WithFields(f).Debugf("contentType: %s", contentType) + + docusignDocument := DocuSignDocument{ + Name: documentName, + DocumentId: documentID, + FileExtension: "pdf", + FileFormatHint: "pdf", + Order: "1", + DocumentBase64: base64.StdEncoding.EncodeToString(pdf), + } + + var envelopeRequest DocuSignEnvelopeRequest + + if callbackURL != "" { + // Webhook properties for callbacks after the user signs the document. + // Ensure that a webhook is returned on the status "Completed" where + // all signers on a document finish signing the document. + log.WithFields(f).Debugf("setting up webhook properties with callback url: %s", callbackURL) + recipientEvents := []DocuSignRecipientEvent{ + { + EnvelopeEventStatusCode: "Completed", + }, + } + + eventNotification := DocuSignEventNotification{ + URL: callbackURL, + LoggingEnabled: true, + EnvelopeEvents: recipientEvents, + } + + envelopeRequest = DocuSignEnvelopeRequest{ + Documents: []DocuSignDocument{ + docusignDocument, + }, + EmailSubject: emailSubject, + EmailBlurb: emailBody, + EventNotification: eventNotification, + Status: "sent", + Recipients: DocuSignRecipientType{ + Signers: []DocuSignRecipient{ + signer, + }, + }, + } + + } else { + + envelopeRequest = DocuSignEnvelopeRequest{ + Documents: []DocuSignDocument{ + docusignDocument, + }, + EmailSubject: emailSubject, + EmailBlurb: emailBody, + Status: "sent", + Recipients: DocuSignRecipientType{ + Signers: []DocuSignRecipient{ + signer, + }, + }, + } + + } + + envelopeResponse, err := s.PrepareSignRequest(ctx, &envelopeRequest) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to create envelope for user: %s", latestSignature.SignatureReferenceID) + return err + } + + log.WithFields(f).Debugf("envelopeID: %s", envelopeResponse.EnvelopeId) + + if !sendAsEmail { + // The URL the user will be redirected to after signing. + // This route will be in charge of extracting the signature's return_url and redirecting. + recipients, recipientErr := s.getEnvelopeRecipients(ctx, envelopeResponse.EnvelopeId) + if recipientErr != nil { + log.WithFields(f).Debugf("unable to fetch recipients for envelope: %s", envelopeResponse.EnvelopeId) + return recipientErr + } + + if len(recipients) == 0 { + log.WithFields(f).Debugf("no envelope recipients found : %s", envelopeResponse.EnvelopeId) + return errors.New("no envelope recipients found") + } + recipient := recipients[0] + returnURL := fmt.Sprintf("%s/v2/return-url/%s", s.ClaV1ApiURL, recipient.ClientUserId) + + log.WithFields(f).Debugf("generating signature sign_url, using return-url as: %s", returnURL) + signURL, signErr := s.GetSignURL(signer.Email, signer.RecipientId, signer.Name, signer.ClientUserId, envelopeResponse.EnvelopeId, returnURL) + + if signErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to get sign url for user: %s", latestSignature.SignatureReferenceID) + return signErr + } + + log.WithFields(f).Debugf("setting signature sign_url as: %s", signURL) + latestSignature.SignatureSignURL = signURL + } + + // Save Envelope ID in signature. + log.WithFields(f).Debugf("saving signature to database...") + latestSignature.SignatureEnvelopeID = envelopeResponse.EnvelopeId + + log.WithFields(f).Debugf("signature: %+v", latestSignature) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to save signature to database for user: %s", latestSignature.SignatureID) + return err + } + + err = s.signatureService.SaveOrUpdateSignature(ctx, latestSignature) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to save signature to database for user: %s", latestSignature.SignatureID) + return err + } + + return nil +} + +type UserSignDetails struct { + userSignatureName string + userSignatureEmail string +} + +func (s *service) populateUserDetails(ctx context.Context, signatureReferenceType string, latestSignature *signatures.ItemSignature, claManagerName, claManagerEmail string, sendAsEmail bool, preferredEmail string) (*UserSignDetails, error) { + f := logrus.Fields{ + "functionName": "sign.populateUserDetails", + } + log.WithFields(f).Debugf("populating user details...") + userSignDetails := &UserSignDetails{ + userSignatureName: Unknown, + userSignatureEmail: Unknown, + } + + if signatureReferenceType == utils.SignatureReferenceTypeCompany { + companyModel, err := s.companyRepo.GetCompany(ctx, latestSignature.SignatureReferenceID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup company by ID: %s", latestSignature.SignatureReferenceID) + return nil, err + } + if companyModel == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup company by ID: %s", latestSignature.SignatureReferenceID) + return nil, errors.New("no CLA manager lookup error") + } + userSignDetails.userSignatureEmail = claManagerEmail + userSignDetails.userSignatureName = claManagerName + + } else if signatureReferenceType == utils.SignatureReferenceTypeUser { + if !sendAsEmail { + userModel, userErr := s.userService.GetUser(latestSignature.SignatureReferenceID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", latestSignature.SignatureReferenceID) + return nil, userErr + } + log.WithFields(f).Debugf("loaded user : %+v", userModel) + + if userModel == nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", latestSignature.SignatureReferenceID) + msg := fmt.Sprintf("No user lookup error for user ID: %s", latestSignature.SignatureReferenceID) + return nil, errors.New(msg) + } + + if userModel.Username != "" { + userSignDetails.userSignatureName = userModel.Username + } + if getUserEmail(userModel, preferredEmail) != "" { + userSignDetails.userSignatureEmail = getUserEmail(userModel, preferredEmail) + } + } + } else { + log.WithFields(f).Warnf("unknown signature reference type: %s", signatureReferenceType) + return nil, errors.New("unknown signature reference type") + } + return userSignDetails, nil +} + +func (s *service) getDocumentResource(urlString string) ([]byte, error) { + + // validate the URL + u, err := url.ParseRequestURI(urlString) + if err != nil { + return nil, err + } + + resp, err := http.Get(u.String()) + if err != nil { + return nil, err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Warnf("error closing response body: %v", err) + } + }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get document resource from url: %s, status code: %d", urlString, resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +// Helper function to extract the docusign tabs from the document +func getTabsFromDocument(document *v1Models.ClaGroupDocument, documentID string, defaultValues map[string]interface{}) DocuSignTab { + var docTab DocuSignTab + f := logrus.Fields{ + "functionName": "sign.getTabsFromDocument", + "documentID": documentID, + } + log.WithFields(f).Debugf("getting tabs from document...") + for _, tab := range document.DocumentTabs { + var args DocuSignTabDetails + args.DocumentId = documentID + args.PageNumber = strconv.FormatInt(tab.DocumentTabPage, 10) + args.XPosition = strconv.FormatInt(tab.DocumentTabPositionx, 10) + args.YPosition = strconv.FormatInt(tab.DocumentTabPositiony, 10) + args.Width = strconv.FormatInt(tab.DocumentTabWidth, 10) + args.Height = strconv.FormatInt(tab.DocumentTabHeight, 10) + args.CustomTabId = tab.DocumentTabID + args.TabLabel = tab.DocumentTabID + args.Name = tab.DocumentTabName + + if tab.DocumentTabAnchorString != "" { + args.AnchorString = tab.DocumentTabAnchorString + args.AnchorIgnoreIfNotPresent = strconv.FormatBool(tab.DocumentTabAnchorIgnoreIfNotPresent) + args.AnchorXOffset = strconv.FormatInt(tab.DocumentTabAnchorxOffset, 10) + args.AnchorYOffset = strconv.FormatInt(tab.DocumentTabAnchoryOffset, 10) + } + + if defaultValues != nil { + if value, ok := defaultValues[tab.DocumentTabID].(string); ok { + args.Value = value + log.WithFields(f).Debugf("setting default value for tab: %s, value: %s", tab.DocumentTabID, value) + } else { + log.WithFields(f).Debugf("no default value found for tab: %s", tab.DocumentTabID) + } + } + + switch tab.DocumentTabType { + case "text": + docTab.TextTabs = append(docTab.TextTabs, args) + case "text_unlocked": + args.Locked = DocSignFalse + docTab.TextTabs = append(docTab.TextTabs, args) + case "text_optional": + args.Required = DocSignFalse + docTab.TextOptionalTabs = append(docTab.TextOptionalTabs, args) + case "number": + docTab.NumberTabs = append(docTab.NumberTabs, args) + case "sign": + docTab.SignHereTabs = append(docTab.SignHereTabs, args) + case "sign_optional": + args.Optional = "true" + docTab.SignHereOptionalTabs = append(docTab.SignHereOptionalTabs, args) + case "date": + docTab.DateSignedTabs = append(docTab.DateSignedTabs, args) + default: + log.WithFields(f).Warnf("unknown document tab type: %s", tab.DocumentTabType) + continue + } + } + + return docTab +} + +// helper function to get user email +func getUserEmail(user *v1Models.User, preferredEmail string) string { + if preferredEmail != "" { + if utils.StringInSlice(preferredEmail, user.Emails) || user.LfEmail == strfmt.Email(preferredEmail) { + return preferredEmail + } + } + if user.LfEmail != "" { + return string(user.LfEmail) + } + if len(user.Emails) > 0 { + return user.Emails[0] + } + return "" +} + +func getActiveSignatureReturnURL(userID string, metadata map[string]interface{}) (string, error) { + + f := logrus.Fields{ + "functionName": "sign.getActiveSignatureReturnURL", + } + + var returnURL string + var err error + var pullRequestID int + var installationID int64 + var repositoryID int64 + + if found, ok := metadata["pull_request_id"].(int); ok { + pullRequestID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get pull request ID for user: %s", userID) + return "", err + } + + if found, ok := metadata["installation_id"].(int64); ok { + installationID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get installation ID for user: %s", userID) + return "", err + } + + if found, ok := metadata["repository_id"].(int64); ok { + repositoryID = found + } else { + log.WithFields(f).WithError(err).Warnf("unable to get repository ID for user: %s", userID) + return "", err + } + + returnURL, err = github.GetReturnURL(context.Background(), installationID, repositoryID, pullRequestID) + + if err != nil { + return "", err + } + + return returnURL, nil + +} + +func (s *service) createDefaultIndividualValues(user *v1Models.User, preferredEmail string) map[string]interface{} { + f := logrus.Fields{ + "functionName": "sign.createDefaultIndiviualValues", + } + log.WithFields(f).Debugf("creating individual default values...for user : %+v", user) + + defaultValues := make(map[string]interface{}) + + if user != nil { + if user.Username != "" { + defaultValues["sign"] = user.Username + defaultValues["full_name"] = user.Username + } + } + + if preferredEmail != "" { + if utils.StringInSlice(preferredEmail, user.Emails) || user.LfEmail == strfmt.Email(preferredEmail) { + defaultValues["email"] = preferredEmail + } + } + + return defaultValues +} + +func (s *service) createDefaultCorporateValues(company *v1Models.Company, signatoryName string, signatoryEmail string, managerName string, managerEmail string) map[string]interface{} { + f := logrus.Fields{ + "functionName": "sign.createDefaultCorporateValues", + } + log.WithFields(f).Debugf("creating corporate default values...") + + defaultValues := make(map[string]interface{}) + + if company != nil { + if company.CompanyName != "" { + defaultValues["corporation"] = company.CompanyName + } + if company.SigningEntityName != "" { + defaultValues["corporation_name"] = company.SigningEntityName + } else { + defaultValues["corporation_name"] = company.CompanyName + } + } + if signatoryName != "" { + defaultValues["signatory_name"] = signatoryName + } + if signatoryEmail != "" { + defaultValues["signatory_email"] = signatoryEmail + } + + if managerName != "" { + defaultValues["point_of_contact"] = managerName + defaultValues["cla_manager_name"] = managerName + } + + if managerEmail != "" { + defaultValues["email"] = managerEmail + defaultValues["cla_manager_email"] = managerEmail + } + + if signatoryName != "" && signatoryEmail != "" { + defaultValues["scheduleA"] = fmt.Sprintf("CLA Manager: %s, %s", signatoryName, signatoryEmail) + } + + return defaultValues +} + +func getLatestSignature(signatures []*v1Models.Signature) *v1Models.Signature { + var latestSignature *v1Models.Signature + for _, signature := range signatures { + if latestSignature == nil { + latestSignature = signature + } else { + if signature.SignatureMajorVersion > latestSignature.SignatureMajorVersion { + latestSignature = signature + } else if signature.SignatureMajorVersion == latestSignature.SignatureMajorVersion { + if signature.SignatureMinorVersion > latestSignature.SignatureMinorVersion { + latestSignature = signature + } + } + } + } + return latestSignature +} + +func (s *service) RequestIndividualSignatureGerrit(ctx context.Context, input *models.IndividualSignatureInput) (*models.IndividualSignatureOutput, error) { + f := logrus.Fields{ + "functionName": "sign.RequestIndividualSignatureGerrit", + "projectID": input.ProjectID, + "userID": input.UserID, + "returnURL": input.ReturnURL, + "returnURLType": input.ReturnURLType, + } + + log.WithFields(f).Debugf("requesting individual signature for user: %s...", *input.UserID) + + // Get the user + user, err := s.userService.GetUser(*input.UserID) + if err != nil || user == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user by ID: %s", *input.UserID) + return nil, err + } + + // Get the project + project, err := s.projectRepo.GetCLAGroupByID(ctx, *input.ProjectID, DontLoadRepoDetails) + if err != nil || project == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project by ID: %s", *input.ProjectID) + return nil, err + } + + // s.ClaV4ApiURL = "https://e08f-102-219-103-105.ngrok-free.app" + callbackURL := fmt.Sprintf("%s/v4/signed/gerrit/individual/%s", s.ClaV4ApiURL, *input.UserID) + + preferredEmail := "" + if user.Emails != nil && len(user.Emails) > 0 { + preferredEmail = user.Emails[0] + } + + defaultValues := s.createDefaultIndividualValues(user, preferredEmail) + + log.WithFields(f).Debugf("defaultValues: %+v", defaultValues) + + sigParams := sigs.GetUserSignaturesParams{ + UserID: *input.UserID, + UserName: &user.Username, + } + userSignatures, err := s.signatureService.GetUserSignatures(ctx, sigParams, input.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user signatures by user ID: %s", *input.UserID) + return nil, err + } + + latestSignature := getLatestSignature(userSignatures.Signatures) + + //loading latest document + log.WithFields(f).Debugf("loading latest individual document for project: %s", *input.ProjectID) + latestDocument, err := common.GetCurrentDocument(ctx, project.ProjectIndividualDocuments) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup latest individual document for project: %s", *input.ProjectID) + return nil, err + } + + if common.AreClaGroupDocumentsEqual(latestDocument, v1Models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warnf("unable to lookup latest individual document for project: %s", *input.ProjectID) + return nil, errors.New("unable to lookup latest individual document for project") + } + + majorVersion, err := strconv.Atoi(latestDocument.DocumentMajorVersion) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document major version to int: %s", latestDocument.DocumentMajorVersion) + return nil, err + } + + minorVersion, err := strconv.Atoi(latestDocument.DocumentMinorVersion) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document minor version to int: %s", latestDocument.DocumentMinorVersion) + return nil, err + } + + // Get gerrits by claGroupID + gerrits, err := s.gerritService.GetClaGroupGerrits(ctx, *input.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup gerrits by project ID: %s", *input.ProjectID) + return nil, err + } + + if len(gerrits.List) == 0 { + log.WithFields(f).Warnf("no gerrits found for project ID: %s", *input.ProjectID) + return nil, errors.New("no gerrits found for project") + } + + returnURL := gerrits.List[0].GerritURL + log.WithFields(f).Debugf("returnURL: %s", returnURL) + + if latestSignature != nil { + log.WithFields(f).Debugf("comparing latest signature document version: %s to latest document version: %s", latestSignature.SignatureDocumentMajorVersion, latestDocument.DocumentMajorVersion) + if latestDocument.DocumentMajorVersion == latestSignature.SignatureDocumentMajorVersion { + + log.WithFields(f).Warnf("user: already has a signature with this project: %s", *input.ProjectID) + + // Regenerate and set the signing URL - This will update the signature record + log.WithFields(f).Debugf("regenerating signing URL for user: %s", *input.UserID) + _, currentTime := utils.CurrentTime() + itemSignature := signatures.ItemSignature{ + SignatureID: latestSignature.SignatureID, + DateModified: currentTime, + SignatureReferenceType: latestSignature.SignatureReferenceType, + SignatureEnvelopeID: latestSignature.SignatureEnvelopeID, + SignatureType: latestSignature.SignatureType, + SignatureReferenceID: latestSignature.SignatureReferenceID, + SignatureProjectID: latestSignature.ProjectID, + SignatureApproved: latestSignature.SignatureApproved, + SignatureSigned: latestSignature.SignatureSigned, + SignatureReferenceName: latestSignature.SignatureReferenceName, + SignatureReferenceNameLower: latestSignature.SignatureReferenceNameLower, + SignedOn: latestSignature.SignedOn, + SignatureReturnURL: string(returnURL), + SignatureReturnURLType: input.ReturnURLType, + SignatureCallbackURL: callbackURL, + SignatureACL: []string{user.LfUsername}, + SignatureDocumentMajorVersion: majorVersion, + SignatureDocumentMinorVersion: minorVersion, + } + signErr := s.populateSignURL(ctx, &itemSignature, callbackURL, "", "", false, "", "", defaultValues, preferredEmail) + if signErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url for user: %s", *input.UserID) + return nil, signErr + } + + return &models.IndividualSignatureOutput{ + SignURL: itemSignature.SignatureSignURL, + SignatureID: latestSignature.SignatureID, + UserID: latestSignature.SignatureReferenceID, + ProjectID: *input.ProjectID, + }, nil + } else { + log.WithFields(f).Debugf("user does NOT have a signature with this project : %s", *input.ProjectID) + } + } + + // Create a new signature object + _, currentTime := utils.CurrentTime() + signatureID := uuid.Must(uuid.NewV4()).String() + + itemSignature := signatures.ItemSignature{ + SignatureID: signatureID, + DateCreated: currentTime, + DateModified: currentTime, + SignatureReferenceType: utils.SignatureReferenceTypeUser, + SignatureSigned: false, + SignatureApproved: true, + SignatureType: utils.SignatureTypeCLA, + SignatureReferenceID: *input.UserID, + SignatureReturnURLType: input.ReturnURLType, + SignatureProjectID: *input.ProjectID, + SignatureReturnURL: string(returnURL), + SignatureCallbackURL: callbackURL, + SignatureACL: []string{user.LfUsername}, + SignatureDocumentMajorVersion: majorVersion, + SignatureDocumentMinorVersion: minorVersion, + SignatureReferenceNameLower: strings.ToLower(getUserName(user)), + } + + log.WithFields(f).Debugf("populating sign url for user: %s...", *input.UserID) + + signErr := s.populateSignURL(ctx, &itemSignature, callbackURL, "", "", false, "", "", defaultValues, preferredEmail) + if signErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url for user: %s", *input.UserID) + return nil, signErr + } + + return &models.IndividualSignatureOutput{ + SignURL: itemSignature.SignatureSignURL, + SignatureID: signatureID, + UserID: *input.UserID, + ProjectID: *input.ProjectID, + }, nil + +} + +func (s *service) requestCorporateSignature(ctx context.Context, apiURL string, input *requestCorporateSignatureInput, comp *v1Models.Company, proj *v1Models.ClaGroup, lfUsername string, currentUserEmail string) (*v1Models.Signature, error) { + f := logrus.Fields{ + "functionName": "requestCorporateSignature", + "apiURL": apiURL, + "CompanyID": input.CompanyID, + "ProjectID": input.ProjectID, + "SigningEntityName": input.SigningEntityName, + "AuthorityName": input.AuthorityName, + "AuthorityEmail": input.AuthorityEmail, + "ReturnURL": input.ReturnURL, + "SendAsEmail": input.SendAsEmail, + } + /** + 1. Ensure User exists in easycla db, if not then create one by getting user by user service + 2. Create individual default values + 3. Load latest document + 4. Check for active corporate signature record for this project/company combination + 5. if signature doesn't exists then Create new signature object + 6. Set signature ACL + 7. Populate sign url + 8. Save signature + **/ + // 1. Ensure User exists in easycla db, if not then create one by getting user by user service + usc := userService.GetClient() + log.WithFields(f).Debugf("Get UserProfile from easycla: %s...", lfUsername) + claUser, err := s.userService.GetUserByUserName(lfUsername, true) + if err != nil { + return nil, err + } + if claUser == nil { + log.WithFields(f).Debugf("Loading user by username from username: %s...", lfUsername) + userModel, userErr := usc.GetUserByUsername(lfUsername) + if userErr != nil { + return nil, userErr + } + var lfEmail string + var emailList []string + emails := userModel.Emails + if len(emails) > 0 { + for _, email := range emails { + if *email.IsPrimary { + lfEmail = *email.EmailAddress + } + emailList = append(emailList, *email.EmailAddress) + } + } + + claUser, err = s.userService.CreateUser(&v1Models.User{ + Username: userModel.Name, + UserExternalID: userModel.ID, + LfUsername: lfUsername, + Admin: false, + LfEmail: strfmt.Email(lfEmail), + Emails: emailList, + }, nil) + if err != nil { + return nil, err + } + } + signatoryName := input.AuthorityName + signatoryEmail := input.AuthorityEmail + + if input.AuthorityName == "" || input.AuthorityEmail == "" { + signatoryName = claUser.Username + signatoryEmail = currentUserEmail + } + + // 2. Create individual default values + log.WithFields(f).Debugf("creating corporate default values...") + defaultValues := s.createDefaultCorporateValues(comp, signatoryName, signatoryEmail, claUser.Username, currentUserEmail) + + // 3. Load latest document + log.WithFields(f).Debugf("loading latest corporate document for project: %s", input.ProjectID) + latestDocument, err := common.GetCurrentDocument(ctx, proj.ProjectCorporateDocuments) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup latest corporate document for project: %s", input.ProjectID) + return nil, err + } + + if common.AreClaGroupDocumentsEqual(latestDocument, v1Models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warnf("unable to lookup latest corporate document for project: %s", input.ProjectID) + return nil, errors.New("unable to lookup latest corporate document for project") + } + + // 4. Check for active corporate signature record for this project/company combination + approved := true + log.WithFields(f).Debug("requestCorporateSignature...") + companySignatures, err := s.signatureService.GetCorporateSignatures(ctx, input.ProjectID, input.CompanyID, &approved, nil) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup user signatures by Company ID: %s, Project ID: %s", input.CompanyID, input.ProjectID) + return nil, err + } + + log.WithFields(f).Debugf("found %d corporate signatures", len(companySignatures)) + + haveSigned := false + for _, s := range companySignatures { + if s.SignatureSigned { + haveSigned = true + break + } + } + if haveSigned { + haveSignedErr := fmt.Errorf("one or more corporate valid signature exists for Company ID: %s, Project ID: %s", input.CompanyID, input.ProjectID) + log.WithFields(f).WithError(err).Warnf(haveSignedErr.Error()) + return nil, haveSignedErr + } + callbackURL := s.getCorporateSignatureCallbackUrl(input.ProjectID, input.CompanyID) + var companySignature *v1Models.Signature + var itemSignature *signatures.ItemSignature + var signed bool + if len(companySignatures) > 0 { + companySignature = companySignatures[0] + log.WithFields(f).Debugf("found %d corporate signatures - using first one with signatureID: %s", len(companySignatures), companySignature.SignatureID) + _, currentTime := utils.CurrentTime() + log.WithFields(f).Debugf("companySignature: %+v", companySignature) + majorVersion := 2 + minorVersion := 0 + var majorVersionErr error + var minorVersionErr error + if companySignature.SignatureDocumentMajorVersion != "" { + majorVersion, majorVersionErr = strconv.Atoi(companySignature.SignatureDocumentMajorVersion) + if majorVersionErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document major version to int: %s", companySignature.SignatureDocumentMajorVersion) + } + } + + if companySignature.SignatureDocumentMinorVersion != "" { + minorVersion, minorVersionErr = strconv.Atoi(companySignature.SignatureDocumentMinorVersion) + if minorVersionErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document minor version to int: %s", companySignature.SignatureDocumentMinorVersion) + } + } + + itemSignature = &signatures.ItemSignature{ + SignatureID: companySignature.SignatureID, + SignatureReferenceType: companySignature.SignatureReferenceType, + SignatureProjectID: companySignature.ProjectID, + SignatureEnvelopeID: companySignature.SignatureEnvelopeID, + SignatureCallbackURL: companySignature.SignatureCallbackURL, + SignatureReturnURL: companySignature.SignatureReturnURL, + SignatureType: companySignature.SignatureType, + SignatoryName: signatoryName, + SignatureSigned: companySignature.SignatureSigned, + SignatureApproved: companySignature.SignatureApproved, + DateCreated: companySignature.Created, + SignatureDocumentMajorVersion: majorVersion, + SignatureDocumentMinorVersion: minorVersion, + SignatureReferenceNameLower: companySignature.SignatureReferenceNameLower, + SigtypeSignedApprovedID: companySignature.SigTypeSignedApprovedID, + DateModified: currentTime, + SignatureReferenceID: companySignature.SignatureReferenceID, + } + + } else { + // 5. if signature doesn't exists then Create new signature object + log.WithFields(f).Debugf("creating new signature object...") + signatureID := uuid.Must(uuid.NewV4()).String() + _, currentTime := utils.CurrentTime() + signed = false + approved = true + majorVersion, majorErr := strconv.Atoi(latestDocument.DocumentMajorVersion) + if majorErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document major version to int: %s", latestDocument.DocumentMajorVersion) + return nil, majorErr + } + minorVersion, minorErr := strconv.Atoi(latestDocument.DocumentMinorVersion) + if minorErr != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert document minor version to int: %s", latestDocument.DocumentMinorVersion) + return nil, minorErr + } + itemSignature = &signatures.ItemSignature{ + SignatureID: signatureID, + SignatureDocumentMajorVersion: majorVersion, + SignatureDocumentMinorVersion: minorVersion, + SignatureReferenceID: comp.CompanyID, + SignatureReferenceType: utils.SignatureReferenceTypeCompany, + SignatureReferenceName: comp.CompanyName, + SignatureProjectID: input.ProjectID, + DateCreated: currentTime, + DateModified: currentTime, + SignatureType: utils.SignatureTypeCCLA, + SignatoryName: signatoryName, + SignatureSigned: false, + SignatureApproved: true, + SignatureCallbackURL: callbackURL, + SignatureReturnURL: input.ReturnURL, + SigtypeSignedApprovedID: fmt.Sprintf("%s#%v#%v#%s", utils.SignatureTypeCCLA, signed, approved, signatureID), + SignatureReferenceNameLower: strings.ToLower(comp.CompanyName), + } + } + + if !input.SendAsEmail { + itemSignature.SignatureReturnURL = input.ReturnURL + } + + // 6. Set signature ACL + log.WithFields(f).Debugf("setting signature ACL...") + itemSignature.SignatureACL = []string{claUser.LfUsername} + + // 7. Populate sign url + log.WithFields(f).Debugf("populating sign url...") + log.WithFields(f).Debugf("itemSignature: %+v", itemSignature) + err = s.populateSignURL(ctx, itemSignature, callbackURL, input.AuthorityName, input.AuthorityEmail, input.SendAsEmail, claUser.Username, currentUserEmail, defaultValues, currentUserEmail) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url for company: %s", input.CompanyID) + return nil, err + } + + return &v1Models.Signature{ + SignatureID: itemSignature.SignatureID, + SignatureSignURL: itemSignature.SignatureSignURL, + }, nil +} + +func removeSignatoryRole(ctx context.Context, userEmail string, companySFID string, projectSFID string) error { + f := logrus.Fields{"functionName": "removeSignatoryRole", "user_email": userEmail, "company_sfid": companySFID, "project_sfid": projectSFID} + log.WithFields(f).Debug("removing role for user") + + usc := userService.GetClient() + // search user + log.WithFields(f).Debug("searching user by email") + user, err := usc.SearchUsersByEmail(userEmail) + if err != nil { + log.WithFields(f).Debug("Failed to get user") + return err + } + + log.WithFields(f).Debug("Getting role id") + acsClient := acsService.GetClient() + roleID, roleErr := acsClient.GetRoleID("cla-signatory") + if roleErr != nil { + log.WithFields(f).Debug("Failed to get role id for cla-signatory") + return roleErr + } + // Get scope id + log.WithFields(f).Debug("getting scope id") + orgClient := organizationService.GetClient() + scopeID, scopeErr := orgClient.GetScopeID(ctx, companySFID, projectSFID, "cla-signatory", "project|organization", user.Username) + + if scopeErr != nil { + log.WithFields(f).Debug("Failed to get scope id for cla-signatory role") + return scopeErr + } + + //Unassign role + log.WithFields(f).Debug("Unassigning role") + deleteErr := orgClient.DeleteOrgUserRoleOrgScopeProjectOrg(ctx, companySFID, roleID, scopeID, &user.Username, &userEmail) + + if deleteErr != nil { + log.WithFields(f).Debug("Failed to remove cla-signatory role") + return deleteErr + } + + return nil + +} + +func prepareUserForSigning(ctx context.Context, userEmail string, companySFID, projectSFID, signedEntityName string) error { + f := logrus.Fields{ + "functionName": "sign.prepareUserForSigning", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "user_email": userEmail, + "company_sfid": companySFID, + "project_sfid": projectSFID, + "signedEntityName": signedEntityName, + } + + role := utils.CLASignatoryRole + log.WithFields(f).Debug("called") + usc := userService.GetClient() + // search user + log.WithFields(f).Debug("searching user by email") + user, err := usc.SearchUsersByEmail(userEmail) + if err != nil { + log.WithFields(f).WithError(err).Debugf("User with email: %s does not have an LF login", userEmail) + return nil + } + + ac := acsService.GetClient() + log.WithFields(f).Debugf("getting role_id for %s", role) + roleID, err := ac.GetRoleID(role) + if err != nil { + log.WithFields(f).WithError(err).Warnf("getting role_id for %s failed: %v", role, err.Error()) + return err + } + log.WithFields(f).Debugf("fetched role %s, role_id %s", role, roleID) + // assign user role of cla signatory for this project + osc := organizationService.GetClient() + + // Attempt to assign the cla-signatory role + log.WithFields(f).Debugf("assigning user role of %s...", role) + err = osc.CreateOrgUserRoleOrgScopeProjectOrg(ctx, userEmail, projectSFID, companySFID, roleID) + if err != nil { + // Log the error - but assigning the cla-signatory role is not a requirement as most users do not have a LF Login - do not throw an error + if strings.Contains(err.Error(), "associated with some organization") { + msg := fmt.Sprintf("user: %s already associated with some organization", user.Username) + log.WithFields(f).WithError(err).Warn(msg) + // return errors.New(msg) + } else if _, ok := err.(*organizations.CreateOrgUsrRoleScopesConflict); !ok { + log.WithFields(f).WithError(err).Warnf("assigning user role of %s failed - user already assigned the role: %v", role, err) + // return err + } else { + log.WithFields(f).WithError(err).Warnf("assigning user role of %s failed: %v", role, err) + } + } + + return nil +} + +func claSignatoryEmailContent(params ClaSignatoryEmailParams) (string, string) { + projectNamesList := strings.Join(params.ProjectNames, ", ") + + emailSubject := fmt.Sprintf("EasyCLA: CLA Signature Request for %s", params.ClaGroupName) + emailBody := fmt.Sprintf("

    Hello %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/ + groups = self.all() + organizations = list(filter(lambda org: org.get_organization_url() == group_url.strip(), groups)) + if organizations: + return organizations[0] + # also cater for potentially missing groups in url + pattern = re.compile(r"(?P\bhttps://gitlab.com/\b)(?P\bgroups\/\b)?(?P\w+)") + match = pattern.search(group_url) + updated_url = '' + if match and not match.group('group'): + cla.log.debug(f'{group_url} missing groups in url. Inserting groups to url ') + parse_url_list = list(match.groups()) + parse_url_list[1] = 'groups/' + updated_url = ''.join(parse_url_list) + if updated_url: + cla.log.debug(f'Updated group_url to : {updated_url}') + organizations = list(filter(lambda org: org.get_organization_url() == updated_url.strip(), groups)) + if organizations: + return organizations[0] + + raise cla.models.DoesNotExist(f"Gitlab Org : {group_url} does not exist") + + def get_organization_by_lower_name(self, organization_name): + organization_name = organization_name.lower() + organization_generator = self.model.organization_name_lower_index.query(organization_name) + organizations = [] + for org_model in organization_generator: + org = GitlabOrg() + org.model = org_model + organizations.append(org) + return organizations + + def all(self): + orgs = self.model.scan() + ret = [] + for organization in orgs: + org = GitlabOrg() + org.model = organization + ret.append(org) + return ret + + class GerritModel(BaseModel): """ Represents a Gerrit Instance in the database. @@ -3739,7 +4320,7 @@ def set_group_name_icla(self, group_name_icla): def set_group_name_ccla(self, group_name_ccla): self.model.group_name_ccla = group_name_ccla - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -3941,7 +4522,7 @@ def set_user_email(self, user_email): def set_status(self, status): self.model.status = status - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -4001,7 +4582,7 @@ def to_dict(self): ret = dict(self.model) return ret - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -4091,7 +4672,7 @@ def get_invites_by_company(self, requested_company_id): invites.append(invite) return invites - def save(self): + def save(self) -> None: self.model.date_modified = datetime.datetime.utcnow() self.model.save() @@ -4114,20 +4695,34 @@ class Meta: event_id = UnicodeAttribute(hash_key=True) event_user_id = UnicodeAttribute(null=True) event_type = UnicodeAttribute(null=True) + + event_cla_group_id = UnicodeAttribute(null=True) + event_cla_group_name = UnicodeAttribute(null=True) + event_cla_group_name_lower = UnicodeAttribute(null=True) + event_project_id = UnicodeAttribute(null=True) + event_project_sfid = UnicodeAttribute(null=True) + event_project_name = UnicodeAttribute(null=True) + event_project_name_lower = UnicodeAttribute(null=True) + event_parent_project_sfid = UnicodeAttribute(null=True) + event_parent_project_name = UnicodeAttribute(null=True) + event_company_id = UnicodeAttribute(null=True) + event_company_sfid = UnicodeAttribute(null=True) event_company_name = UnicodeAttribute(null=True) event_company_name_lower = UnicodeAttribute(null=True) - event_project_name = UnicodeAttribute(null=True) - event_project_name_lower = UnicodeAttribute(null=True) + event_user_name = UnicodeAttribute(null=True) event_user_name_lower = UnicodeAttribute(null=True) - event_time = UTCDateTimeAttribute(default=datetime.datetime.now()) + + event_time = UTCDateTimeAttribute(default=datetime.datetime.utcnow()) event_time_epoch = NumberAttribute(default=int(time.time())) + event_date = UnicodeAttribute(null=True) + event_data = UnicodeAttribute(null=True) + event_data_lower = UnicodeAttribute(null=True) event_summary = UnicodeAttribute(null=True) - event_date = UnicodeAttribute(null=True) - event_project_external_id = UnicodeAttribute(null=True) + event_date_and_contains_pii = UnicodeAttribute(null=True) company_id_external_project_id = UnicodeAttribute(null=True) contains_pii = BooleanAttribute(null=True) @@ -4145,8 +4740,11 @@ def __init__( event_id=None, event_type=None, user_id=None, + event_cla_group_id=None, + event_cla_group_name=None, event_project_id=None, event_company_id=None, + event_company_sfid=None, event_data=None, event_summary=None, event_company_name=None, @@ -4159,48 +4757,74 @@ def __init__( self.model = EventModel() self.model.event_id = event_id self.model.event_type = event_type + self.model.event_user_id = user_id - self.model.event_project_id = event_project_id - self.model.event_company_id = event_company_id - self.model.event_data = event_data - self.model.event_summary = event_summary - self.model.event_company_name = event_company_name - self.model.contains_pii = contains_pii - if self.model.event_company_name: - self.model.event_company_name_lower = self.model.event_company_name.lower() self.model.event_user_name = event_user_name if self.model.event_user_name: self.model.event_user_name_lower = self.model.event_user_name.lower() + + self.model.event_cla_group_id = event_cla_group_id + self.model.event_cla_group_name = event_cla_group_name + if self.model.event_cla_group_name: + self.model.event_cla_group_name_lower = self.model.event_cla_group_name.lower() + + self.model.event_project_id = event_project_id self.model.event_project_name = event_project_name if self.model.event_project_name: self.model.event_project_name_lower = self.model.event_project_name.lower() + self.model.event_company_id = event_company_id + self.model.event_company_sfid = event_company_sfid + self.model.event_company_name = event_company_name + if self.model.event_company_name: + self.model.event_company_name_lower = self.model.event_company_name.lower() + + self.model.event_data = event_data + if self.model.event_data: + self.model.event_data_lower = self.model.event_data.lower() + self.model.event_summary = event_summary + self.model.contains_pii = contains_pii + def __str__(self): return ( f"id:{self.model.event_id}, " f"event type:{self.model.event_type}, " + f"event_user id:{self.model.event_user_id}, " + f"event user name: {self.model.event_user_name}," + + f"event cla group id:{self.model.event_cla_group_id}, " + f"event cla group name:{self.model.event_cla_group_name}, " + f"event project id:{self.model.event_project_id}, " + f"event project sfid: {self.model.event_project_sfid}," + f"event project name: {self.model.event_project_name}, " + f"event parent project sfid:{self.model.event_parent_project_sfid}, " + f"event parent project name: {self.model.event_parent_project_name}, " + f"event company id: {self.model.event_company_id}, " + f"event company sfid: {self.model.event_company_sfid}, " + f"event company name: {self.model.event_company_name}, " + f"event time: {self.model.event_time}, " f"event time epoch: {self.model.event_time_epoch}, " + f"event date: {self.model.event_date}," + f"event data: {self.model.event_data}, " f"event summary: {self.model.event_summary}, " - f"event company name: {self.model.event_company_name}, " - f"event project name: {self.model.event_project_name}, " - f"event user name: {self.model.event_user_name}," - f"event date: {self.model.event_date}," - f"event project external id: {self.model.event_project_external_id}," f"contains pii: {self.model.contains_pii}" ) 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() + def delete(self): + self.model.delete() + def load(self, event_id): try: event = self.model.get(str(event_id)) @@ -4208,58 +4832,85 @@ def load(self, event_id): raise cla.models.DoesNotExist("Event not found") self.model = event - def get_event_company_id(self): - return self.model.event_company_id + def get_event_date_created(self) -> str: + return self.model.date_created - def get_event_company_name(self): - return self.model.event_company_name + def get_event_date_modified(self) -> str: + return self.model.date_modified - def get_event_user_id(self): + def get_event_user_id(self) -> str: return self.model.event_user_id - def get_event_data(self): + def get_event_data(self) -> str: return self.model.event_data - def get_event_summary(self): + def get_event_data_lower(self) -> str: + return self.model.event_data_lower + + def get_event_summary(self) -> str: return self.model.event_summary - def get_event_date(self): + def get_event_date(self) -> str: return self.model.event_date - def get_event_id(self): + def get_event_id(self) -> str: return self.model.event_id - def get_event_project_id(self): + def get_event_cla_group_id(self) -> str: + return self.model.event_cla_group_id + + def get_event_cla_group_name(self) -> str: + return self.model.event_cla_group_name + + def get_event_cla_group_name_lower(self) -> str: + return self.model.event_cla_group_name_lower + + def get_event_project_id(self) -> str: return self.model.event_project_id - def get_event_project_name(self): + def get_event_project_sfid(self) -> str: + return self.model.event_project_sfid + + def get_event_project_name(self) -> str: return self.model.event_project_name - def get_event_project_name_lower(self): + def get_event_project_name_lower(self) -> str: return self.model.event_project_name_lower - def get_event_type(self): + def get_event_parent_project_sfid(self) -> str: + return self.model.event_parent_project_sfid + + def get_event_parent_project_name(self) -> str: + return self.model.event_parent_project_name + + def get_event_type(self) -> str: return self.model.event_type - def get_event_time(self): + def get_event_time(self) -> str: return self.model.date_created - def get_event_time_epoch(self): + def get_event_time_epoch(self) -> int: return self.model.event_time_epoch - def get_event_company_name_lower(self): + def get_event_company_id(self) -> str: + return self.model.event_company_id + + def get_event_company_sfid(self) -> str: + return self.model.event_company_sfid + + def get_event_company_name(self) -> str: + return self.model.event_company_name + + def get_event_company_name_lower(self) -> str: return self.model.event_company_name_lower - def get_event_user_name(self): + def get_event_user_name(self) -> str: return self.model.event_user_name - def get_event_user_name_lower(self): + def get_event_user_name_lower(self) -> str: return self.model.event_user_name_lower - def get_event_project_external_id(self): - return self.model.event_project_external_id - - def get_company_id_external_project_id(self): + def get_company_id_external_project_id(self) -> str: return self.model.company_id_external_project_id def all(self, ids=None): @@ -4274,54 +4925,173 @@ def all(self, ids=None): ret.append(ev) return ret - def set_event_company_id(self, company_id): - self.model.event_company_id = company_id - - def set_event_company_name(self, company_name): - self.model.event_company_name = company_name - if company_name: - self.model.event_company_name_lower = company_name.lower() + def all_limit(self, limit: Optional[int] = None, last_evaluated_key: Optional[str] = None): + result_iterator = self.model.scan(limit=limit, last_evaluated_key=last_evaluated_key) + ret = [] + for signature in result_iterator: + evt = Event() + evt.model = signature + ret.append(evt) + return ret, result_iterator.last_evaluated_key, result_iterator.total_count + + def search_missing_event_data_lower(self, limit: Optional[int] = None, last_evaluated_key: Optional[str] = None): + filter_condition = (EventModel.event_data_lower.does_not_exist()) + projection = ["event_id", "event_data", "event_data_lower"] + result_iterator = self.model.scan(limit=limit, + last_evaluated_key=last_evaluated_key, + filter_condition=filter_condition, + attributes_to_get=projection) + ret = [] + for signature in result_iterator: + evt = Event() + evt.model = signature + ret.append(evt) + return ret, result_iterator.last_evaluated_key, result_iterator.total_count + + # def search_by_year(self, year: str, limit: Optional[int] = None, last_evaluated_key: Optional[str] = None): + # filter_condition = (EventModel.event_date.contains(year)) + # projection = ["event_id", "event_date"] + # result_iterator = self.model.scan(limit=limit, + # last_evaluated_key=last_evaluated_key, + # filter_condition=filter_condition, + # attributes_to_get=projection) + # ret = [] + # for signature in result_iterator: + # evt = Event() + # evt.model = signature + # ret.append(evt) + # return ret, result_iterator.last_evaluated_key, result_iterator.total_count + + def get_events_type_by_week(self, event_type: EventType) -> dict: + filter_attributes = { + "event_type": event_type.name, + } + filter_condition = create_filter(filter_attributes, EventModel) + projection = ["event_id", "event_type", "date_created"] + cla.log.debug(f'querying events using filter: {filter_condition}...') + result_iterator = self.model.scan(filter_condition=filter_condition, attributes_to_get=projection) + + ret = {} + + for event_record in result_iterator: + date_time_value = cla.utils.get_time_from_string(str(event_record.date_created)) + year = date_time_value.year + week_number = date_time_value.isocalendar()[1] + cla.log.debug(f'processing events - ' + f'{event_record.event_id} - ' + f'{event_record.event_type} - ' + f'{event_record.date_created} - ' + f'{year} - {week_number:02d}') + key = f'{year} {week_number:02d}' + if key in ret: + ret[key] += 1 + else: + ret[key] = 1 + return ret - def set_event_data(self, event_data): + def set_event_data(self, event_data: str): self.model.event_data = event_data + self.model.event_data_lower = event_data.lower() + + def set_event_data_lower(self, event_data: str): + if event_data: + self.model.event_data_lower = event_data.lower() - def set_event_summary(self, event_summary): + def set_event_summary(self, event_summary: str): self.model.event_summary = event_summary - def set_event_id(self, event_id): + def set_event_id(self, event_id: str): self.model.event_id = event_id - def set_event_user_id(self, user_id): + def set_event_company_id(self, company_id: str): + self.model.event_company_id = company_id + + def set_event_company_sfid(self, company_sfid: str): + self.model.event_company_sfid = company_sfid + + def set_event_company_name(self, company_name: str): + self.model.event_company_name = company_name + if company_name: + self.model.event_company_name_lower = company_name.lower() + + def set_event_user_id(self, user_id: str): self.model.event_user_id = user_id - def set_event_project_id(self, event_project_id): + def set_event_cla_group_id(self, event_cla_group_id: str): + self.model.event_cla_group_id = event_cla_group_id + + def set_event_cla_group_name(self, event_cla_group_name: str): + self.model.event_cla_group_name = event_cla_group_name + if event_cla_group_name: + self.model.event_cla_group_name_lower = event_cla_group_name.lower() + + def set_event_project_id(self, event_project_id: str): self.model.event_project_id = event_project_id - def set_event_project_name(self, event_project_name): + def set_event_project_sfid(self, event_project_sfid: str): + self.model.event_project_sfid = event_project_sfid + + def set_event_project_name(self, event_project_name: str): self.model.event_project_name = event_project_name if event_project_name: self.model.event_project_name_lower = event_project_name.lower() - def set_event_type(self, event_type): + def set_event_parent_project_sfid(self, event_parent_project_sfid: str): + self.model.event_parent_project_sfid = event_parent_project_sfid + + def set_event_parent_project_name(self, event_parent_project_name: str): + self.model.event_parent_project_name = event_parent_project_name + + def set_event_type(self, event_type: str): self.model.event_type = event_type - def set_event_user_name(self, event_user_name): + def set_event_user_name(self, event_user_name: str): self.model.event_user_name = event_user_name self.model.event_user_name_lower = event_user_name.lower() - def set_event_project_external_id(self, event_project_external_id): - self.model.event_project_external_id = event_project_external_id - - def set_event_date_and_contains_pii(self, contains_pii=False): + def set_event_date_and_contains_pii(self, contains_pii: bool = False): dateDDMMYYYY = datetime.date.today().strftime("%d-%m-%Y") self.model.contains_pii = contains_pii self.model.event_date = dateDDMMYYYY self.model.event_date_and_contains_pii = '{}#{}'.format(dateDDMMYYYY, str(contains_pii).lower()) def set_company_id_external_project_id(self): - if self.model.event_project_external_id is not None and self.model.event_company_id is not None: + if self.model.event_project_sfid is not None and self.model.event_company_id is not None: self.model.company_id_external_project_id = (f'{self.model.event_company_id}' - f'#{self.model.event_project_external_id}') + f'#{self.model.event_project_sfid}') + + @staticmethod + def set_cla_group_details(event, cla_group_id: str): + try: + project = Project() + project.load(str(cla_group_id)) + event.set_event_cla_group_id(cla_group_id) + event.set_event_cla_group_name(project.get_project_name()) + event.set_event_project_sfid(project.get_project_external_id()) + Event.set_project_details(event, project.get_project_external_id()) + except Exception as err: + cla.log.warning(f'unable to set CLA Group name due to the following error: {err}') + + @staticmethod + def set_project_details(event, event_project_id: str): + try: + sf_project = ProjectService.get_project_by_id(event_project_id) + if sf_project is not None: + event.set_event_project_name(sf_project.get("Name")) + # Does this project have a parent? + if sf_project.get("Parent") is not None: + # Load the parent to get the name + Event.set_project_parent_details(event, sf_project.get("Parent")) + except Exception as err: + cla.log.warning(f'unable to set project name and parent ID/name ' + f'due to the following error: {err}') + + @staticmethod + def set_project_parent_details(event, event_parent_project_id: str): + sf_project = ProjectService.get_project_by_id(event_parent_project_id) + if sf_project is not None: + event.set_event_parent_project_sfid(sf_project.get("ID")) + event.set_event_parent_project_name(sf_project.get("Name")) def search_events(self, **kwargs): """ @@ -4367,27 +5137,46 @@ def search_events(self, **kwargs): @classmethod def create_event( cls, - event_type=None, - event_project_id=None, - event_company_id=None, - event_project_name=None, - event_company_name=None, - event_data=None, - event_summary=None, - event_user_id=None, - contains_pii=False, - dry_run=False + event_type: Optional[EventType] = None, + event_cla_group_id: Optional[str] = None, + event_project_id: Optional[str] = None, + event_company_id: Optional[str] = None, + event_project_name: Optional[str] = None, + event_company_name: Optional[str] = None, + event_data: Optional[str] = None, + event_summary: Optional[str] = None, + event_user_id: Optional[str] = None, + event_user_name: Optional[str] = None, + contains_pii: bool = False, + dry_run: bool = False ): """ Creates an event returns the newly created event in dict format. :param event_type: The type of event :type event_type: EventType - :param event_user_id: The user that is assocaited with the event - :type event_user_id: string :param event_project_id: The project associated with event :type event_project_id: string - + :param event_cla_group_id: The CLA Group ID associated with event + :type event_cla_group_id: string + :param event_project_name: The project name associated with event + :type event_project_name: string + :param event_company_id: The company associated with event + :type event_company_id: string + :param event_company_name: The company name associated with event + :type event_company_name: string + :param event_data: The event message/data + :type event_data: string + :param event_summary: The event summary message/data + :type event_summary: string + :param event_user_id: The user that is associated with the event + :type event_user_id: string + :param event_user_name: The user's name that is associated with the event + :type event_user_name: string + :param contains_pii: flag to indicate if the message contains personal information (deprecated) + :type contains_pii: bool + :param dry_run: flag to indicate this is for testing and the record should not be stored/created + :type dry_run: bool """ try: event = cls() @@ -4395,16 +5184,17 @@ def create_event( event_project_name = "undefined" if event_company_name is None: event_company_name = "undefined" - if event_project_id: - try: - project = Project() - project.load(str(event_project_id)) - event_project_name = project.get_project_name() - event_project_external_id = project.get_project_external_id() - event.set_event_project_id(event_project_id) - event.set_event_project_external_id(event_project_external_id) - except DoesNotExist as err: - return {"errors": {"event_project_id": str(err)}} + + # Handle case where teh event_project_id == CLA Group ID or SalesForce ID + if event_project_id and is_uuidv4(event_project_id): # cla group id in the project_id field + Event.set_cla_group_details(event, event_project_id) + elif event_project_id and not is_uuidv4(event_project_id): # external SFID + Event.set_project_details(event, event_project_id) + + # if the caller has given us a CLA Group ID + if event_cla_group_id is not None: # cla_group_id + Event.set_cla_group_details(event, event_cla_group_id) + if event_company_id: try: company = Company() @@ -4413,6 +5203,7 @@ def create_event( event.set_event_company_id(event_company_id) except DoesNotExist as err: return {"errors": {"event_company_id": str(err)}} + if event_user_id: try: user = User() @@ -4423,15 +5214,18 @@ def create_event( event.set_event_user_name(user_name) except DoesNotExist as err: return {"errors": {"event_": str(err)}} + + if event_user_name: + event.set_event_user_name(event_user_name) + event.set_event_id(str(uuid.uuid4())) if event_type: event.set_event_type(event_type.name) - event.set_event_project_name(event_project_name) + event.set_event_project_name(event_project_name) # potentially overrides the SF Name event.set_event_summary(event_summary) event.set_event_company_name(event_company_name) event.set_event_data(event_data) event.set_event_date_and_contains_pii(contains_pii) - event.set_company_id_external_project_id() if not dry_run: event.save() return {"data": event.to_dict()} @@ -4613,3 +5407,34 @@ def all(self): ccla_whitelist_request.model = request ret.append(ccla_whitelist_request) return ret + + +def patched_deserialize(self, values): + """ + Decode from list of AttributeValue types. This is a patched version of the pynamodb version 3.4.1 which address + the use-case where it attempts to iterate over a NoneType value. This is a known issue in pynamodb and has been + resolved in the latest 5.x series of pynamodb. However, if we upgrade to this version it will break all the + date/time processing in our models. So, we simply patch this version of the library to address this issue. + """ + deserialized_lst = [] + if not values: + return deserialized_lst + for v in values: + class_for_deserialize = self.element_type() if self.element_type else _get_class_for_deserialize(v) + attr_value = _get_value_for_deserialize(v) + deserialized_lst.append(class_for_deserialize.deserialize(attr_value)) + return deserialized_lst + + +def _get_value_for_deserialize(value): + key = next(iter(value.keys())) + if key == 'NULL': + return None + return value[key] + + +def _get_class_for_deserialize(value): + value_type = list(value.keys())[0] + if value_type not in DESERIALIZE_CLASS_MAP: + raise ValueError('Unknown value: ' + str(value)) + return DESERIALIZE_CLASS_MAP[value_type] diff --git a/cla-backend/cla/models/event_types.py b/cla-backend/cla/models/event_types.py index cd6d03fe6..4f6ca5d66 100644 --- a/cla-backend/cla/models/event_types.py +++ b/cla-backend/cla/models/event_types.py @@ -6,9 +6,8 @@ class EventType(Enum): """ - Enumeraters representing type of CLA events + Enumerator representing type of CLA events across projects, users, signatures, whitelists - """ CreateUser = "Create User" UpdateUser = "Update User" @@ -24,7 +23,7 @@ class EventType(Enum): CreateProjectDocumentTemplate = "Create Project Document with Template" DeleteProjectDocument = "Delete Project Document" AddPermission = "Add Permission" - RemovePermission = "Remove Pemrission" + RemovePermission = "Remove Permission" AddProjectManager = "Add Project Manager" RemoveProjectManager = "Remove Project Manager" RequestCompanyWL = "Request Company Whitelist" diff --git a/cla-backend/cla/models/github_models.py b/cla-backend/cla/models/github_models.py index d94d053bf..889c560f2 100644 --- a/cla-backend/cla/models/github_models.py +++ b/cla-backend/cla/models/github_models.py @@ -4,22 +4,32 @@ """ Holds the GitHub repository service. """ +import concurrent.futures import json import os +import threading +import time import uuid -from typing import List, Union, Optional +from typing import List, Optional, Union +import cla import falcon import github +from cla.controllers.github_application import GitHubInstallation +from cla.models import DoesNotExist, repository_service_interface +from cla.models.dynamo_models import GitHubOrg, Repository +from cla.user import UserCommitSummary +from cla.utils import (append_project_version_to_url, get_project_instance, + set_active_pr_metadata) from github import PullRequest -from github.GithubException import UnknownObjectException, BadCredentialsException, GithubException +from github.GithubException import (BadCredentialsException, GithubException, + IncompletableObject, + RateLimitExceededException, + UnknownObjectException) from requests_oauthlib import OAuth2Session -import cla -from cla.controllers.github_application import GitHubInstallation -from cla.models import repository_service_interface, DoesNotExist -from cla.models.dynamo_models import Repository, GitHubOrg -from cla.utils import get_project_instance, append_project_version_to_url +# some emails we want to exclude when we register the users +EXCLUDE_GITHUB_EMAILS = ["noreply.github.com"] class GitHub(repository_service_interface.RepositoryService): @@ -55,80 +65,95 @@ def get_repository_id(self, repo_name, installation_id=None): try: return self.client.get_repo(repo_name).id except github.GithubException as err: - cla.log.error('Could not find GitHub repository (%s), ensure it exists and that ' - 'your personal access token is configured with the repo scope', repo_name) + cla.log.error( + "Could not find GitHub repository (%s), ensure it exists and that " + "your personal access token is configured with the repo scope", + repo_name, + ) except Exception as err: - cla.log.error('Unknown error while getting GitHub repository ID for repository %s: %s', - repo_name, str(err)) + cla.log.error("Unknown error while getting GitHub repository ID for repository %s: %s", repo_name, str(err)) def received_activity(self, data): - cla.log.debug('github_models.received_activity - Received GitHub activity: %s', data) - if 'pull_request' not in data: - cla.log.debug('github_models.received_activity - Activity not related to pull request - ignoring') - return {'message': 'Not a pull request - no action performed'} - if data['action'] == 'opened': - cla.log.debug('github_models.received_activity - Handling opened pull request') + cla.log.debug("github_models.received_activity - Received GitHub activity: %s", data) + if "pull_request" not in data and "merge_group" not in data: + cla.log.debug("github_models.received_activity - Activity not related to pull request - ignoring") + return {"message": "Not a pull request nor a merge group - no action performed"} + if data["action"] == "opened": + cla.log.debug("github_models.received_activity - Handling opened pull request") return self.process_opened_pull_request(data) - elif data['action'] == 'reopened': - cla.log.debug('github_models.received_activity - Handling reopened pull request') + elif data["action"] == "reopened": + cla.log.debug("github_models.received_activity - Handling reopened pull request") return self.process_reopened_pull_request(data) - elif data['action'] == 'closed': - cla.log.debug('github_models.received_activity - Handling closed pull request') + elif data["action"] == "closed": + cla.log.debug("github_models.received_activity - Handling closed pull request") return self.process_closed_pull_request(data) - elif data['action'] == 'synchronize': - cla.log.debug('github_models.received_activity - Handling synchronized pull request') + elif data["action"] == "synchronize": + cla.log.debug("github_models.received_activity - Handling synchronized pull request") return self.process_synchronized_pull_request(data) + elif data["action"] == "checks_requested": + cla.log.debug("github_models.received_activity - Handling checks requested pull request") + return self.process_checks_requested_merge_group(data) else: - cla.log.debug('github_models.received_activity - Ignoring unsupported action: {}'.format(data['action'])) + cla.log.debug("github_models.received_activity - Ignoring unsupported action: {}".format(data["action"])) def sign_request(self, installation_id, github_repository_id, change_request_id, request): """ This method gets called when the OAuth2 app (NOT the GitHub App) needs to get info on the user trying to sign. In this case we begin an OAuth2 exchange with the 'user:email' scope. """ - fn = 'github_models.sign_request' # function name - cla.log.debug(f'{fn} - Initiating GitHub sign request for installation_id: {installation_id}, ' - f'for repository {github_repository_id}, ' - f'for PR: {change_request_id}') + fn = "github_models.sign_request" # function name + cla.log.debug( + f"{fn} - Initiating GitHub sign request for installation_id: {installation_id}, " + f"for repository {github_repository_id}, " + f"for PR: {change_request_id}" + ) # Not sure if we need a different token for each installation ID... - cla.log.debug(f'{fn} - Loading session from request: {request}...') + cla.log.debug(f"{fn} - Loading session from request: {request}...") session = self._get_request_session(request) - cla.log.debug(f'{fn} - Adding github details to session...') - session['github_installation_id'] = installation_id - session['github_repository_id'] = github_repository_id - session['github_change_request_id'] = change_request_id + cla.log.debug(f"{fn} - Adding github details to session: {session} which is type: {type(session)}...") + session["github_installation_id"] = installation_id + session["github_repository_id"] = github_repository_id + session["github_change_request_id"] = change_request_id - cla.log.debug(f'{fn} - Determining return URL from the inbound request...') + cla.log.debug(f"{fn} - Determining return URL from the inbound request...") origin_url = self.get_return_url(github_repository_id, change_request_id, installation_id) - cla.log.debug(f'{fn} - Return URL from the inbound request is {origin_url}') - session['github_origin_url'] = origin_url + cla.log.debug(f"{fn} - Return URL from the inbound request is {origin_url}") + session["github_origin_url"] = origin_url cla.log.debug(f'{fn} - Stored origin url in session as session["github_origin_url"] = {origin_url}') - if 'github_oauth2_token' in session: - cla.log.debug(f'{fn} - Using existing session GitHub OAuth2 token') - return self.redirect_to_console( - installation_id, github_repository_id, change_request_id, - origin_url, request) + if "github_oauth2_token" in session: + cla.log.debug(f"{fn} - Using existing session GitHub OAuth2 token") + return self.redirect_to_console(installation_id, github_repository_id, change_request_id, origin_url, request) else: - cla.log.debug(f'{fn} - No existing GitHub OAuth2 token - building authorization url and state') - authorization_url, state = self.get_authorization_url_and_state(installation_id, - github_repository_id, - int(change_request_id), - ['user:email']) - cla.log.debug(f'{fn} - Obtained GitHub OAuth2 state from authorization - storing state in the session...') - session['github_oauth2_state'] = state - cla.log.debug(f'{fn} - GitHub OAuth2 request with state {state} - sending user to {authorization_url}') + cla.log.debug(f"{fn} - No existing GitHub OAuth2 token - building authorization url and state") + authorization_url, state = self.get_authorization_url_and_state( + installation_id, github_repository_id, int(change_request_id), ["user:email"] + ) + cla.log.debug(f"{fn} - Obtained GitHub OAuth2 state from authorization - storing state in the session...") + session["github_oauth2_state"] = state + cla.log.debug(f"{fn} - GitHub OAuth2 request with state {state} - sending user to {authorization_url}") raise falcon.HTTPFound(authorization_url) - def _get_request_session(self, request): # pylint: disable=no-self-use + def _get_request_session(self, request) -> dict: # pylint: disable=no-self-use """ Mockable method used to get the current user session. """ - # return request.context['session'] - session = request.context.get('session') + fn = "cla.models.github_models._get_request_session" + session = request.context.get("session") if session is None: - cla.log.warning(f'Session is empty for request: {request}') + cla.log.warning(f"Session is empty for request: {request}") + cla.log.debug(f"{fn} - loaded session: {session}") + + # Ensure session is a dict - getting issue where session is a string + if isinstance(session, str): + # convert string to a dict + cla.log.debug(f"{fn} - session is type: {type(session)} - converting to dict...") + session = json.loads(session) + # Reset the session now that we have converted it to a dict + request.context["session"] = session + cla.log.debug(f"{fn} - session: {session} which is now type: {type(session)}...") + return session def get_authorization_url_and_state(self, installation_id, github_repository_id, pull_request_number, scope): @@ -149,20 +174,21 @@ def get_authorization_url_and_state(self, installation_id, github_repository_id, # Get the PR's html_url property. # origin = self.get_return_url(github_repository_id, pull_request_number, installation_id) # Add origin to user's session here? - fn = 'github_models.get_authorization_url_and_state' - redirect_uri = os.environ.get('CLA_API_BASE', '').strip() + "/v2/github/installation" - github_oauth_url = cla.conf['GITHUB_OAUTH_AUTHORIZE_URL'] - github_oauth_client_id = os.environ['GH_OAUTH_CLIENT_ID'] - - cla.log.debug(f'{fn} - Directing user to the github authorization url: {github_oauth_url} via ' - f'our github installation flow: {redirect_uri}' - f'using the github oauth client id: {github_oauth_client_id[0:5]} ' - f'with scope: {scope}') + fn = "github_models.get_authorization_url_and_state" + redirect_uri = os.environ.get("CLA_API_BASE", "").strip() + "/v2/github/installation" + github_oauth_url = cla.conf["GITHUB_OAUTH_AUTHORIZE_URL"] + github_oauth_client_id = os.environ["GH_OAUTH_CLIENT_ID"] + + cla.log.debug( + f"{fn} - Directing user to the github authorization url: {github_oauth_url} via " + f"our github installation flow: {redirect_uri} " + f"using the github oauth client id: {github_oauth_client_id[0:5]} " + f"with scope: {scope}" + ) - return self._get_authorization_url_and_state(client_id=github_oauth_client_id, - redirect_uri=redirect_uri, - scope=scope, - authorize_url=github_oauth_url) + return self._get_authorization_url_and_state( + client_id=github_oauth_client_id, redirect_uri=redirect_uri, scope=scope, authorize_url=github_oauth_url + ) def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url): """ @@ -178,48 +204,49 @@ def oauth2_redirect(self, state, code, request): # pylint: disable=too-many-arg It will handle storing the OAuth2 session information for this user for further requests and initiate the signing workflow. """ - fn = 'github_models.oauth2_redirect' - cla.log.debug(f'{fn} - handling GitHub OAuth2 redirect with request: {dir(request)}') + fn = "github_models.oauth2_redirect" + cla.log.debug(f"{fn} - handling GitHub OAuth2 redirect with request: {dir(request)}") session = self._get_request_session(request) # request.context['session'] - cla.log.debug(f'{fn} - state: {state}, code: {code}, session: {session}') + cla.log.debug(f"{fn} - state: {state}, code: {code}, session: {session}") - if 'github_oauth2_state' in session: - session_state = session['github_oauth2_state'] + if "github_oauth2_state" in session: + session_state = session["github_oauth2_state"] else: session_state = None - cla.log.warning(f'{fn} - github_oauth2_state not set in current session') + cla.log.warning(f"{fn} - github_oauth2_state not set in current session") if state != session_state: - cla.log.warning(f'{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}') - raise falcon.HTTPBadRequest('Invalid OAuth2 state', state) + cla.log.warning(f"{fn} - invalid GitHub OAuth2 state {session_state} expecting {state}") + raise falcon.HTTPBadRequest("Invalid OAuth2 state", state) # Get session information for this request. - cla.log.debug(f'{fn} - attempting to fetch OAuth2 token for state {state}') - installation_id = session.get('github_installation_id', None) - github_repository_id = session.get('github_repository_id', None) - change_request_id = session.get('github_change_request_id', None) - origin_url = session.get('github_origin_url', None) - state = session.get('github_oauth2_state') - token_url = cla.conf['GITHUB_OAUTH_TOKEN_URL'] - client_id = os.environ['GH_OAUTH_CLIENT_ID'] - client_secret = os.environ['GH_OAUTH_SECRET'] - cla.log.debug(f'{fn} - fetching token using {client_id[0:5]}... with state={state}, token_url={token_url}, ' - f'client_secret={client_secret[0:5]}, with code={code}') + cla.log.debug(f"{fn} - attempting to fetch OAuth2 token for state {state}") + installation_id = session.get("github_installation_id", None) + github_repository_id = session.get("github_repository_id", None) + change_request_id = session.get("github_change_request_id", None) + origin_url = session.get("github_origin_url", None) + state = session.get("github_oauth2_state") + token_url = cla.conf["GITHUB_OAUTH_TOKEN_URL"] + client_id = os.environ["GH_OAUTH_CLIENT_ID"] + client_secret = os.environ["GH_OAUTH_SECRET"] + cla.log.debug( + f"{fn} - fetching token using {client_id[0:5]}... with state={state}, token_url={token_url}, " + f"client_secret={client_secret[0:5]}, with code={code}" + ) token = self._fetch_token(client_id, state, token_url, client_secret, code) - cla.log.debug(f'{fn} - oauth2 token received for state {state}: {token} - storing token in session') - session['github_oauth2_token'] = token - cla.log.debug(f'{fn} - redirecting the user back to the console: {origin_url}') + cla.log.debug(f"{fn} - oauth2 token received for state {state}: {token} - storing token in session") + session["github_oauth2_token"] = token + cla.log.debug(f"{fn} - redirecting the user back to the console: {origin_url}") return self.redirect_to_console(installation_id, github_repository_id, change_request_id, origin_url, request) def redirect_to_console(self, installation_id, repository_id, pull_request_id, origin_url, request): - fn = 'github_models.redirect_to_console' - console_endpoint = cla.conf['CONTRIBUTOR_BASE_URL'] - console_v2_endpoint = cla.conf['CONTRIBUTOR_V2_BASE_URL'] + fn = "github_models.redirect_to_console" + console_endpoint = cla.conf["CONTRIBUTOR_BASE_URL"] + console_v2_endpoint = cla.conf["CONTRIBUTOR_V2_BASE_URL"] # Get repository using github's repository ID. repository = Repository().get_repository_by_external_id(repository_id, "github") if repository is None: - cla.log.warning(f'{fn} - Could not find repository with the following ' - f'repository_id: {repository_id}') + cla.log.warning(f"{fn} - Could not find repository with the following " f"repository_id: {repository_id}") return None # Get project ID from this repository @@ -229,7 +256,7 @@ def redirect_to_console(self, installation_id, repository_id, pull_request_id, o project = get_project_instance() project.load(str(project_id)) except DoesNotExist as err: - return {'errors': {'project_id': str(err)}} + return {"errors": {"project_id": str(err)}} user = self.get_or_create_user(request) # Ensure user actually requires a signature for this project. @@ -242,35 +269,48 @@ def redirect_to_console(self, installation_id, repository_id, pull_request_id, o # try: # document = cla.utils.get_project_latest_individual_document(project_id) # except DoesNotExist: - # cla.log.debug('No ICLA for project %s' %project_id) + # cla.log.debug('No ICLA for project %s' %project_idSignature) # if signature is not None and \ # signature.get_signature_document_major_version() == document.get_document_major_version(): # return cla.utils.redirect_user_by_signature(user, signature) # Store repository and PR info so we can redirect the user back later. cla.utils.set_active_signature_metadata(user.get_user_id(), project_id, repository_id, pull_request_id) - console_url = '' + console_url = "" # Temporary condition until all CLA Groups are ready for the v2 Contributor Console - if project.get_version() == 'v2': + if project.get_version() == "v2": # Generate url for the v2 console - console_url = 'https://' + console_v2_endpoint + \ - '/#/cla/project/' + project_id + \ - '/user/' + user.get_user_id() + \ - '?redirect=' + origin_url - cla.log.debug(f'{fn} - redirecting to v2 console: {console_url}...') + console_url = ( + "https://" + + console_v2_endpoint + + "/#/cla/project/" + + project_id + + "/user/" + + user.get_user_id() + + "?redirect=" + + origin_url + ) + cla.log.debug(f"{fn} - redirecting to v2 console: {console_url}...") else: # Generate url for the v1 contributor console - console_url = 'https://' + console_endpoint + \ - '/#/cla/project/' + project_id + \ - '/user/' + user.get_user_id() + \ - '?redirect=' + origin_url - cla.log.debug(f'{fn} - redirecting to v1 console: {console_url}...') + console_url = ( + "https://" + + console_endpoint + + "/#/cla/project/" + + project_id + + "/user/" + + user.get_user_id() + + "?redirect=" + + origin_url + ) + cla.log.debug(f"{fn} - redirecting to v1 console: {console_url}...") raise falcon.HTTPFound(console_url) - def _fetch_token(self, client_id, state, token_url, client_secret, - code): # pylint: disable=too-many-arguments,no-self-use + def _fetch_token( + self, client_id, state, token_url, client_secret, code + ): # pylint: disable=too-many-arguments,no-self-use """ Mockable method to fetch a OAuth2Session token. """ @@ -281,16 +321,20 @@ def sign_workflow(self, installation_id, github_repository_id, pull_request_numb Once we have the 'github_oauth2_token' value in the user's session, we can initiate the signing workflow. """ - fn = 'sign_workflow' - cla.log.warning(f'{fn} - Initiating GitHub signing workflow for ' - f'GitHub repo {github_repository_id} ' - f'with PR: {pull_request_number}') + fn = "sign_workflow" + cla.log.warning( + f"{fn} - Initiating GitHub signing workflow for " + f"GitHub repo {github_repository_id} " + f"with PR: {pull_request_number}" + ) user = self.get_or_create_user(request) signature = cla.utils.get_user_signature_by_github_repository(installation_id, user) project_id = cla.utils.get_project_id_from_installation_id(installation_id) document = cla.utils.get_project_latest_individual_document(project_id) - if signature is not None and \ - signature.get_signature_document_major_version() == document.get_document_major_version(): + if ( + signature is not None + and signature.get_signature_document_major_version() == document.get_document_major_version() + ): return cla.utils.redirect_user_by_signature(user, signature) else: # Signature not found or older version, create new one and send user to sign. @@ -303,11 +347,27 @@ def process_opened_pull_request(self, data): :param data: The data returned from GitHub on this webhook. :type data: dict """ - pull_request_id = data['pull_request']['number'] - github_repository_id = data['repository']['id'] - installation_id = data['installation']['id'] + pull_request_id = data["pull_request"]["number"] + github_repository_id = data["repository"]["id"] + installation_id = data["installation"]["id"] self.update_change_request(installation_id, github_repository_id, pull_request_id) + def process_checks_requested_merge_group(self, data): + """ + Helper method to handle a webhook fired from GitHub for a merge group event. + + :param data: The data returned from GitHub on this webhook. + :type data: dict + """ + merge_group_sha = data["merge_group"]["head_sha"] + github_repository_id = data["repository"]["id"] + installation_id = data["installation"]["id"] + pull_request_message = data["merge_group"]["head_commit"]["message"] + + # Extract the pull request number from the message + pull_request_id = cla.utils.extract_pull_request_number(pull_request_message) + self.update_merge_group(installation_id, github_repository_id, merge_group_sha, pull_request_id) + def process_easycla_command_comment(self, data): """ Processes easycla command comment if present @@ -319,9 +379,11 @@ def process_easycla_command_comment(self, data): raise ValueError("missing comment body, ignoring the message") if "/easycla" not in comment_str.split(): - raise ValueError("unsupported comment supplied, currently only /easycla command is supported") + raise ValueError( + f"unsupported comment supplied: {comment_str.split()}, " "currently only the '/easycla' command is supported" + ) - github_repository_id = data.get('repository', {}).get('id', None) + github_repository_id = data.get("repository", {}).get("id", None) if not github_repository_id: raise ValueError("missing github repository id in pull request comment") cla.log.debug(f"comment trigger for github_repo : {github_repository_id}") @@ -332,8 +394,8 @@ def process_easycla_command_comment(self, data): raise ValueError("missing pull request id ") cla.log.debug(f"comment trigger for pull_request_id : {pull_request_id}") - cla.log.debug("installation object : ", data.get('installation', {})) - installation_id = data.get('installation', {}).get('id', None) + cla.log.debug("installation object : ", data.get("installation", {})) + installation_id = data.get("installation", {}).get("id", None) if not installation_id: raise ValueError("missing installation id in pull request comment") cla.log.debug(f"comment trigger for installation_id : {installation_id}") @@ -344,64 +406,397 @@ def get_return_url(self, github_repository_id, change_request_id, installation_i pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) return pull_request.html_url + def get_existing_repository(self, github_repository_id): + fn = "get_existing_repository" + # Queries GH for the complete repository details, see: + # https://developer.github.com/v3/repos/#get-a-repository + cla.log.debug(f"{fn} - fetching repository details for GH repo ID: {github_repository_id}...") + repository = Repository().get_repository_by_external_id(str(github_repository_id), "github") + if repository is None: + cla.log.warning(f"{fn} - unable to locate repository by GH ID: {github_repository_id}") + return None + + if repository.get_enabled() is False: + cla.log.warning(f"{fn} - repository is disabled, skipping: {github_repository_id}") + return None + + cla.log.debug(f"{fn} - found repository by GH ID: {github_repository_id}") + return repository + + def check_org_validity(self, installation_id, repository): + fn = "check_org_validity" + organization_name = repository.get_organization_name() + + # Check that the Github Organization exists in our database + cla.log.debug(f"{fn} - fetching organization details for GH org name: {organization_name}...") + github_org = GitHubOrg() + try: + github_org.load(organization_name=organization_name) + except DoesNotExist as err: + cla.log.warning(f"{fn} - unable to locate organization by GH name: {organization_name}") + return False + + if github_org.get_organization_installation_id() != installation_id: + cla.log.warning( + f"{fn} - " + f"the installation ID: {github_org.get_organization_installation_id()} " + f"of this organization does not match installation ID: {installation_id} " + "given by the pull request." + ) + return False + + cla.log.debug(f"{fn} - found organization by GH name: {organization_name}") + return True + + def get_pull_request_retry(self, github_repository_id, change_request_id, installation_id, retries=3) -> dict: + """ + Helper function to retry getting a pull request from GitHub. + """ + fn = "get_pull_request_retry" + pull_request = {} + for i in range(retries): + try: + # check if change_request_id is a valid int + _ = int(change_request_id) + pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) + except ValueError as ve: + cla.log.error( + f"{fn} - Invalid PR: {change_request_id} - error: {ve}. Unable to fetch " + f"PR {change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) + if i <= retries: + cla.log.debug(f"{fn} - attempt {i + 1} - waiting to retry...") + time.sleep(2) + continue + else: + cla.log.warning( + f"{fn} - attempt {i + 1} - exhausted retries - unable to load PR " + f"{change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) + # TODO: DAD - possibly update the PR status? + return + # Fell through - no error, exit loop and continue on + break + cla.log.debug(f"{fn} - retrieved pull request: {pull_request}") + + return pull_request + + def update_merge_group_status( + self, installation_id, repository_id, pull_request, merge_commit_sha, signed, missing, project_version + ): + """ + Helper function to update a merge queue entrys status based on the list of signers. + :param installation_id: The ID of the GitHub installation + :type installation_id: int + :param repository_id: The ID of the GitHub repository this PR belongs to. + :type repository_id: int + :param pull_request: The GitHub PullRequest object for this PR. + """ + fn = "update_merge_group_status" + context_name = os.environ.get("GH_STATUS_CTX_NAME") + if context_name is None: + context_name = "communitybridge/cla" + if missing is not None and len(missing) > 0: + state = "failure" + context, body = cla.utils.assemble_cla_status(context_name, signed=False) + sign_url = cla.utils.get_full_sign_url( + "github", str(installation_id), repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) + elif signed is not None and len(signed) > 0: + state = "success" + # For status, we change the context from author_name to 'communitybridge/cla' or the + # specified default value per issue #166 + context, body = cla.utils.assemble_cla_status(context_name, signed=True) + sign_url = cla.conf["CLA_LANDING_PAGE"] # Remove this once signature detail page ready. + sign_url = os.path.join(sign_url, "#/") + sign_url = append_project_version_to_url(address=sign_url, project_version=project_version) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) + else: + # error condition - should have at least one committer, and they would be in one of the above + # lists: missing or signed + state = "failure" + # For status, we change the context from author_name to 'communitybridge/cla' or the + # specified default value per issue #166 + context, body = cla.utils.assemble_cla_status(context_name, signed=False) + sign_url = cla.utils.get_full_sign_url( + "github", str(installation_id), repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) + cla.log.warning( + "{fn} - This is an error condition - " + f"should have at least one committer in one of these lists: " + f"{len(signed)} passed, {missing}" + ) + + # Create the commit status on the merge commit + if self.client is None: + self.client = self._get_github_client(installation_id) + + # Get repository + cla.log.debug(f"{fn} - Getting repository by ID: {repository_id}") + repository = self.client.get_repo(int(repository_id)) + + # Get the commit object + cla.log.debug(f"{fn} - Getting commit by SHA: {merge_commit_sha}") + commit_obj = repository.get_commit(merge_commit_sha) + + cla.log.debug( + f"{fn} - Creating commit status for merge commit: {merge_commit_sha} " + f"with state: {state}, context: {context}, body: {body}" + ) + + create_commit_status_for_merge_group(commit_obj, merge_commit_sha, state, sign_url, body, context) + + def update_merge_group(self, installation_id, github_repository_id, merge_group_sha, pull_request_id): + fn = "update_queue_entry" + + # Note: late 2021/early 2022 we observed that sometimes we get the event for a PR, then go back to GitHub + # to query for the PR details and discover the PR is 404, not available for some reason. Added retry + # logic to retry a couple of times to address any timing issues. + + try: + # Get the pull request details from GitHub + cla.log.debug( + f"{fn} - fetching pull request details for GH repo ID: {github_repository_id} " + f"PR ID: {pull_request_id}..." + ) + pull_request = self.get_pull_request_retry(github_repository_id, pull_request_id, installation_id) + except Exception as e: + cla.log.warning( + f"{fn} - unable to load PR {pull_request_id} from GitHub repository " + f"{github_repository_id} using installation id {installation_id} - error: {e}" + ) + return + + try: + # Get Commit authors + commit_authors = get_pull_request_commit_authors(pull_request, installation_id) + cla.log.debug(f"{fn} - commit authors: {commit_authors}") + except Exception as e: + cla.log.warning( + f"{fn} - unable to load commit authors for PR {pull_request_id} from GitHub repository " + f"{github_repository_id} using installation id {installation_id} - error: {e}" + ) + return + + try: + # Get existing repository info using the repository's external ID, + # which is the repository ID assigned by github. + cla.log.debug(f"{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}") + repository = Repository().get_repository_by_external_id(github_repository_id, "github") + if repository is None: + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Failed to load GitHub repository by " + f"id: {github_repository_id} in our DB - repository reference is None - " + "Is this org/repo configured in the Project Console?" + " Unable to update status." + ) + # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA + # app/bot is enabled in GitHub (which is why we received the event in the first place), but the + # repository is not setup/configured in EasyCLA from the administration console + return + + # If the repository is not enabled in our database, we don't process it. + if not repository.get_enabled(): + cla.log.warning( + f"{fn} - repository {repository.get_repository_url()} associated with " + f"PR: {pull_request.number} is NOT enabled" + " - ignoring PR request" + ) + # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA + # app/bot is enabled in GitHub (which is why we received the event in the first place), but the + # repository is NOT enabled in the administration console + return + + except DoesNotExist: + cla.log.warning( + f"{fn} - PR: {pull_request.number}, could not find repository with the " + f"repository ID: {github_repository_id}" + ) + cla.log.warning( + f"{fn} - PR: {pull_request.number}, failed to update change request of " + f"repository {github_repository_id} - returning" + ) + return + + # Get GitHub Organization name that the repository is configured to. + organization_name = repository.get_repository_organization_name() + cla.log.debug(f"{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}") + + # Check that the GitHub Organization exists. + github_org = GitHubOrg() + try: + github_org.load(organization_name) + except DoesNotExist: + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Could not find Github Organization " + f"with the following organization name: {organization_name}" + ) + cla.log.warning( + f"{fn}- PR: {pull_request.number}, Failed to update change request of " + f"repository {github_repository_id} - returning" + ) + return + + # Ensure that installation ID for this organization matches the given installation ID + if github_org.get_organization_installation_id() != installation_id: + cla.log.warning( + f"{fn} - PR: {pull_request.number}, " + f"the installation ID: {github_org.get_organization_installation_id()} " + f"of this organization does not match installation ID: {installation_id} " + "given by the pull request." + ) + cla.log.error( + f"{fn} - PR: {pull_request.number}, Failed to update change request " + f"of repository {github_repository_id} - returning" + ) + return + + project_id = repository.get_repository_project_id() + project = get_project_instance() + project.load(project_id) + + signed = [] + missing = [] + + # Check if the user has signed the CLA + cla.log.debug(f"{fn} - checking if the user has signed the CLA...") + for user_commit_summary in commit_authors: + handle_commit_from_user(project, user_commit_summary, signed, missing) + + # update Merge group status + self.update_merge_group_status( + installation_id, github_repository_id, pull_request, merge_group_sha, signed, missing, project.get_version() + ) + def update_change_request(self, installation_id, github_repository_id, change_request_id): - fn = 'update_change_request' + fn = "update_change_request" # Queries GH for the complete pull request details, see: # https://developer.github.com/v3/pulls/#response-1 - try: - # check if change_request_id is a valid int - _ = int(change_request_id) - pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) - except ValueError: - cla.log.error(f'{fn} - Invalid PR: {change_request_id} . (Unable to cast to integer) ') - return - cla.log.debug(f'{fn} - retrieved pull request: {pull_request}') - # Get all unique users/authors involved in this PR - returns a list of - # (commit_sha_string, (author_id, author_username, author_email) tuples - commit_authors = get_pull_request_commit_authors(pull_request) + # Note: late 2021/early 2022 we observed that sometimes we get the event for a PR, then go back to GitHub + # to query for the PR details and discover the PR is 404, not available for some reason. Added retry + # logic to retry a couple of times to address any timing issues. + pull_request = {} + tries = 3 + for i in range(tries): + try: + # check if change_request_id is a valid int + _ = int(change_request_id) + pull_request = self.get_pull_request(github_repository_id, change_request_id, installation_id) + except ValueError as ve: + cla.log.error( + f"{fn} - Invalid PR: {change_request_id} - error: {ve}. Unable to fetch " + f"PR {change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) + if i <= tries: + cla.log.debug(f"{fn} - attempt {i + 1} - waiting to retry...") + time.sleep(2) + continue + else: + cla.log.warning( + f"{fn} - attempt {i + 1} - exhausted retries - unable to load PR " + f"{change_request_id} from GitHub repository {github_repository_id} " + f"using installation id {installation_id}." + ) + # TODO: DAD - possibly update the PR status? + return + # Fell through - no error, exit loop and continue on + break + cla.log.debug(f"{fn} - retrieved pull request: {pull_request}") + + # Get all unique users/authors involved in this PR - returns a List[UserCommitSummary] objects + commit_authors = get_pull_request_commit_authors(pull_request, installation_id) + + cla.log.debug( + f"{fn} - PR: {pull_request.number}, found {len(commit_authors)} unique commit authors " + f"for pull request: {pull_request.number}" + ) try: # Get existing repository info using the repository's external ID, # which is the repository ID assigned by github. - cla.log.debug(f'{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}') + cla.log.debug(f"{fn} - PR: {pull_request.number}, Loading GitHub repository by id: {github_repository_id}") repository = Repository().get_repository_by_external_id(github_repository_id, "github") if repository is None: - cla.log.warning(f'{fn} - PR: {pull_request.number}, Failed to load GitHub repository by ' - f'id: {github_repository_id} in our DB - repository reference is None - ' - 'Is this org/repo configured in the Project Console?' - ' Unable to update status.') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Failed to load GitHub repository by " + f"id: {github_repository_id} in our DB - repository reference is None - " + "Is this org/repo configured in the Project Console?" + " Unable to update status." + ) + # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA + # app/bot is enabled in GitHub (which is why we received the event in the first place), but the + # repository is not setup/configured in EasyCLA from the administration console + return + + # If the repository is not enabled in our database, we don't process it. + if not repository.get_enabled(): + cla.log.warning( + f"{fn} - repository {repository.get_repository_url()} associated with " + f"PR: {pull_request.number} is NOT enabled" + " - ignoring PR request" + ) + # Optionally, we could add a comment or add a status to the PR informing the users that the EasyCLA + # app/bot is enabled in GitHub (which is why we received the event in the first place), but the + # repository is NOT enabled in the administration console return + except DoesNotExist: - cla.log.warning(f'{fn} - PR: {pull_request.number}, could not find repository with the ' - f'repository ID: {github_repository_id}') - cla.log.warning(f'{fn} - PR: {pull_request.number}, failed to update change request of ' - f'repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, could not find repository with the " + f"repository ID: {github_repository_id}" + ) + cla.log.warning( + f"{fn} - PR: {pull_request.number}, failed to update change request of " + f"repository {github_repository_id} - returning" + ) return - # Get Github Organization name that the repository is configured to. + # Get GitHub Organization name that the repository is configured to. organization_name = repository.get_repository_organization_name() - cla.log.debug(f'{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}') + cla.log.debug(f"{fn} - PR: {pull_request.number}, determined github organization is: {organization_name}") - # Check that the Github Organization exists. + # Check that the GitHub Organization exists. github_org = GitHubOrg() try: github_org.load(organization_name) except DoesNotExist: - cla.log.warning(f'{fn} - PR: {pull_request.number}, Could not find Github Organization ' - f'with the following organization name: {organization_name}') - cla.log.warning(f'{fn}- PR: {pull_request.number}, Failed to update change request of ' - f'repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, Could not find Github Organization " + f"with the following organization name: {organization_name}" + ) + cla.log.warning( + f"{fn}- PR: {pull_request.number}, Failed to update change request of " + f"repository {github_repository_id} - returning" + ) return # Ensure that installation ID for this organization matches the given installation ID if github_org.get_organization_installation_id() != installation_id: - cla.log.warning(f'{fn} - PR: {pull_request.number}, ' - f'the installation ID: {github_org.get_organization_installation_id()} ' - f'of this organization does not match installation ID: {installation_id} ' - 'given by the pull request.') - cla.log.error(f'{fn} - PR: {pull_request.number}, Failed to update change request ' - f'of repository {github_repository_id} - returning') + cla.log.warning( + f"{fn} - PR: {pull_request.number}, " + f"the installation ID: {github_org.get_organization_installation_id()} " + f"of this organization does not match installation ID: {installation_id} " + "given by the pull request." + ) + cla.log.error( + f"{fn} - PR: {pull_request.number}, Failed to update change request " + f"of repository {github_repository_id} - returning" + ) return # Retrieve project ID from the repository. @@ -409,26 +804,46 @@ def update_change_request(self, installation_id, github_repository_id, change_re project = get_project_instance() project.load(str(project_id)) + try: + # Save entry into the cla-{stage}-store table for active PRs + set_active_pr_metadata( + github_author_username=pull_request.user.login, + github_author_email=pull_request.user.email, + cla_group_id=project.get_project_id(), + repository_id=str(github_repository_id), + pull_request_id=str(change_request_id), + ) + except Exception as e: + cla.log.error(f"{fn} - problem saving PR metadata for PR: {pull_request.number}, error: {e}") + # Find users who have signed and who have not signed. signed = [] missing = [] + futures = [] + + cla.log.debug( + f"{fn} - PR: {pull_request.number}, scanning users - " "determining who has signed a CLA an who has not." + ) + + with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: + for user_commit_summary in commit_authors: + cla.log.debug(f"{fn} - PR: {pull_request.number} for user: {user_commit_summary}") + futures.append(executor.submit(handle_commit_from_user, project, user_commit_summary, signed, missing)) + + # Wait for all threads to be finished before moving on + executor.shutdown(wait=True) + + for future in concurrent.futures.as_completed(futures): + cla.log.debug(f"{fn} - ThreadClosed for handle_commit_from_user") - cla.log.debug(f'{fn} - PR: {pull_request.number}, scanning users - ' - 'determining who has signed a CLA an who has not.') - for commit_sha, author_info in commit_authors: - # Extract the author info tuple details - author_id = author_info[0] - author_username = author_info[1] - author_email = author_info[2] - cla.log.debug(f'{fn} - PR: {pull_request.number}, ' - f'processing sha: {commit_sha} ' - f'from author id: {author_id}, username: {author_username}, email: {author_email}') - handle_commit_from_user(project, commit_sha, author_info, signed, missing) - - cla.log.debug(f'{fn} - PR: {pull_request.number}, ' - f'updating github pull request for repo: {github_repository_id}, ' - f'with signed authors: {signed} ' - f'with missing authors: {missing}') + # At this point, the signed and missing lists are now filled and updated with the commit user info + + cla.log.debug( + f"{fn} - PR: {pull_request.number}, " + f"updating github pull request for repo: {github_repository_id}, " + f"with signed authors: {signed} " + f"with missing authors: {missing}" + ) repository_name = repository.get_repository_name() update_pull_request( installation_id=installation_id, @@ -437,7 +852,8 @@ def update_change_request(self, installation_id, github_repository_id, change_re repository_name=repository_name, signed=signed, missing=missing, - project_version=project.get_version()) + project_version=project.get_version(), + ) def get_pull_request(self, github_repository_id, pull_request_number, installation_id): """ @@ -450,18 +866,47 @@ def get_pull_request(self, github_repository_id, pull_request_number, installati :param installation_id: The ID of the GitHub application installed on this repository. :type installation_id: int | None """ - cla.log.debug('Getting PR %s from GitHub repository %s', pull_request_number, github_repository_id) + cla.log.debug("Getting PR %s from GitHub repository %s", pull_request_number, github_repository_id) if self.client is None: self.client = get_github_integration_client(installation_id) repo = self.client.get_repo(int(github_repository_id)) try: return repo.get_pull(int(pull_request_number)) except UnknownObjectException: - cla.log.error('Could not find pull request %s for repository %s - ensure it ' - 'exists and that your personal access token has the "repo" scope enabled', - pull_request_number, github_repository_id) + cla.log.error( + "Could not find pull request %s for repository %s - ensure it " + 'exists and that your personal access token has the "repo" scope enabled', + pull_request_number, + github_repository_id, + ) + except BadCredentialsException as err: + cla.log.error("Invalid GitHub credentials provided: %s", str(err)) + + def get_github_user_by_email(self, email, installation_id): + """ + Helper method to get the GitHub user object from GitHub. + + :param email: The email of the GitHub user. + :type email: string + :param name: The name of the GitHub user. + :type name: string + :param installation_id: The ID of the GitHub application installed on this repository. + :type installation_id: int | None + """ + cla.log.debug("Getting GitHub user %s", email) + if self.client is None: + self.client = get_github_integration_client(installation_id) + try: + cla.log.debug("Searching for GitHub user by email handle: %s", email) + users_by_email = self.client.search_users(f"{email} in:email") + if len(list(users_by_email)) == 0: + cla.log.debug("No GitHub user found with email handle: %s", email) + return None + return list(users_by_email)[0] + except UnknownObjectException: + cla.log.error("Could not find GitHub user %s", email) except BadCredentialsException as err: - cla.log.error('Invalid GitHub credentials provided: %s', str(err)) + cla.log.error("Invalid GitHub credentials provided: %s", str(err)) def get_or_create_user(self, request): """ @@ -470,34 +915,40 @@ def get_or_create_user(self, request): :param request: The hug request object for this API call. :type request: Request """ + fn = "github_models.get_or_create_user" session = self._get_request_session(request) - github_user = self.get_user_data(session, os.environ['GH_OAUTH_CLIENT_ID']) - if 'error' in github_user: + github_user = self.get_user_data(session, os.environ["GH_OAUTH_CLIENT_ID"]) + if "error" in github_user: # Could not get GitHub user data - maybe user revoked CLA app permissions? session = self._get_request_session(request) - del session['github_oauth2_state'] - del session['github_oauth2_token'] - cla.log.warning('Deleted OAuth2 session data - retrying token exchange next time') - raise falcon.HTTPError('400 Bad Request', 'github_oauth2_token', - 'Token permissions have been rejected, please try again') - emails = self.get_user_emails(session, os.environ['GH_OAUTH_CLIENT_ID']) + del session["github_oauth2_state"] + del session["github_oauth2_token"] + cla.log.warning(f"{fn} - Deleted OAuth2 session data - retrying token exchange next time") + raise falcon.HTTPError( + "400 Bad Request", "github_oauth2_token", "Token permissions have been rejected, please try again" + ) + + emails = self.get_user_emails(session, os.environ["GH_OAUTH_CLIENT_ID"]) if len(emails) < 1: - cla.log.warning('GitHub user has no verified email address: %s (%s)', - github_user['name'], github_user['login']) + cla.log.warning( + f"{fn} - GitHub user has no verified email address: %s (%s)", github_user["name"], github_user["login"] + ) raise falcon.HTTPError( - '412 Precondition Failed', 'email', - 'Please verify at least one email address with GitHub') + "412 Precondition Failed", "email", "Please verify at least one email address with GitHub" + ) - cla.log.debug('Trying to load GitHub user by GitHub ID: %s', github_user['id']) - users = cla.utils.get_user_instance().get_user_by_github_id(github_user['id']) + cla.log.debug(f"{fn} - Trying to load GitHub user by GitHub ID: %s", github_user["id"]) + users = cla.utils.get_user_instance().get_user_by_github_id(github_user["id"]) if users is not None: # Users search can return more than one match - so it's an array - we set the first record value for now?? user = users[0] - cla.log.debug('Loaded GitHub user by GitHub ID: %s - %s (%s)', - user.get_user_name(), - user.get_user_emails(), - user.get_user_github_id()) + cla.log.debug( + f"{fn} - Loaded GitHub user by GitHub ID: %s - %s (%s)", + user.get_user_name(), + user.get_user_emails(), + user.get_user_github_id(), + ) # update/set the github username if available cla.utils.update_github_username(github_user, user) @@ -507,7 +958,7 @@ def get_or_create_user(self, request): return user # User not found by GitHub ID, trying by email. - cla.log.debug('Could not find GitHub user by GitHub ID: %s', github_user['id']) + cla.log.debug(f"{fn} - Could not find GitHub user by GitHub ID: %s", github_user["id"]) # TODO: This is very slow and needs to be improved - may need a DB schema change. users = None user = cla.utils.get_user_instance() @@ -520,27 +971,29 @@ def get_or_create_user(self, request): # Users search can return more than one match - so it's an array - we set the first record value for now?? user = users[0] # Found user by email, setting the GitHub ID - user.set_user_github_id(github_user['id']) + user.set_user_github_id(github_user["id"]) # update/set the github username if available cla.utils.update_github_username(github_user, user) user.set_user_emails(emails) user.save() - cla.log.debug(f'Loaded GitHub user by email: {user}') + cla.log.debug(f"{fn} - Loaded GitHub user by email: {user}") return user # User not found, create. - cla.log.debug(f'Could not find GitHub user by email: {emails}') - cla.log.debug(f'Creating new GitHub user {github_user["name"]} - ' - f'({github_user["id"]}/{github_user["login"]}), ' - f'emails: {emails}') + cla.log.debug(f"{fn} - Could not find GitHub user by email: {emails}") + cla.log.debug( + f'{fn} - Creating new GitHub user {github_user["name"]} - ' + f'({github_user["id"]}/{github_user["login"]}), ' + f"emails: {emails}" + ) user = cla.utils.get_user_instance() user.set_user_id(str(uuid.uuid4())) user.set_user_emails(emails) - user.set_user_name(github_user['name']) - user.set_user_github_id(github_user['id']) - user.set_user_github_username(github_user['login']) + user.set_user_name(github_user["name"]) + user.set_user_github_id(github_user["id"]) + user.set_user_github_username(github_user["login"]) user.save() return user @@ -554,17 +1007,22 @@ def get_user_data(self, session, client_id): # pylint: disable=no-self-use :param client_id: The GitHub OAuth2 client ID. :type client_id: string """ - token = session['github_oauth2_token'] + fn = "cla.models.github_models.get_user_data" + token = session.get("github_oauth2_token") + if token is None: + cla.log.error(f"{fn} - unable to load github_oauth2_token from session, session is: {session}") + return {"error": "could not get user data from session"} + oauth2 = OAuth2Session(client_id, token=token) - request = oauth2.get('https://api.github.com/user') + request = oauth2.get("https://api.github.com/user") github_user = request.json() - cla.log.debug('GitHub user data: %s', github_user) - if 'message' in github_user: - cla.log.error('Could not get user data with OAuth2 token: %s', github_user['message']) - return {'error': 'Could not get user data: %s' % github_user['message']} + cla.log.debug(f"{fn} - GitHub user data: %s", github_user) + if "message" in github_user: + cla.log.error(f'{fn} - Could not get user data with OAuth2 token: {github_user["message"]}') + return {"error": "Could not get user data: %s" % github_user["message"]} return github_user - def get_user_emails(self, session, client_id) -> Union[List[str], dict]: # pylint: disable=no-self-use + def get_user_emails(self, session: dict, client_id: str) -> Union[List[str], dict]: # pylint: disable=no-self-use """ Mockable method to get all user emails based on OAuth2 session. @@ -574,20 +1032,29 @@ def get_user_emails(self, session, client_id) -> Union[List[str], dict]: # pyli :type client_id: string """ emails = self._fetch_github_emails(session=session, client_id=client_id) - cla.log.debug('GitHub user emails: %s', emails) - if 'error' in emails: + cla.log.debug("GitHub user emails: %s", emails) + if "error" in emails: return emails - return [item['email'] for item in emails if item['verified']] + + verified_emails = [item["email"] for item in emails if item["verified"]] + excluded_emails = [email for email in verified_emails if any([email.endswith(e) for e in EXCLUDE_GITHUB_EMAILS])] + included_emails = [email for email in verified_emails if not any([email.endswith(e) for e in EXCLUDE_GITHUB_EMAILS])] + + if len(included_emails) > 0: + return included_emails + + # something we're not very happy about but probably it can happen + return excluded_emails def get_primary_user_email(self, request) -> Union[Optional[str], dict]: """ gets the user primary email from the registered emails from the github api """ - fn = 'github_models.get_primary_user_email' + fn = "github_models.get_primary_user_email" try: - cla.log.debug(f'{fn} - Fetching Github primary email') + cla.log.debug(f"{fn} - fetching Github primary email") session = self._get_request_session(request) - client_id = os.environ['GH_OAUTH_CLIENT_ID'] + client_id = os.environ["GH_OAUTH_CLIENT_ID"] emails = self._fetch_github_emails(session=session, client_id=client_id) if "error" in emails: return None @@ -596,29 +1063,29 @@ def get_primary_user_email(self, request) -> Union[Optional[str], dict]: if email.get("verified", False) and email.get("primary", False): return email["email"] except Exception as e: - cla.log.warning(f'{fn} - lookup failed - {e} - returning None') + cla.log.warning(f"{fn} - lookup failed - {e} - returning None") return None return None - def _fetch_github_emails(self, session, client_id) -> Union[List[dict], dict]: + def _fetch_github_emails(self, session: dict, client_id: str) -> Union[List[dict], dict]: """ Method is responsible for fetching the user emails from /user/emails endpoint :param session: :param client_id: :return: """ - fn = 'github_models._fetch_github_emails' # function name + fn = "github_models._fetch_github_emails" # function name # Use the user's token to fetch their public email(s) - don't use the system token as this endpoint won't work # as expected - token = session.get('github_oauth2_token') + token = session.get("github_oauth2_token") if token is None: - cla.log.warning(f'{fn} - unable to load github_oauth2_token from the session - session is empty') + cla.log.warning(f"{fn} - unable to load github_oauth2_token from the session - session is empty") oauth2 = OAuth2Session(client_id, token=token) - request = oauth2.get('https://api.github.com/user/emails') + request = oauth2.get("https://api.github.com/user/emails") resp = request.json() - if 'message' in resp: + if "message" in resp: cla.log.warning(f'{fn} - could not get user emails with OAuth2 token: {resp["message"]}') - return {'error': 'Could not get user emails: %s' % resp['message']} + return {"error": "Could not get user emails: %s" % resp["message"]} return resp def process_reopened_pull_request(self, data): @@ -668,80 +1135,91 @@ def create_repository(data): repository = cla.utils.get_repository_instance() repository.set_repository_id(str(uuid.uuid4())) # TODO: Need to use an ID unique across all repository providers instead of namespace. - full_name = data['repository']['full_name'] - namespace = full_name.split('/')[0] + full_name = data["repository"]["full_name"] + namespace = full_name.split("/")[0] repository.set_repository_project_id(namespace) - repository.set_repository_external_id(data['repository']['id']) + repository.set_repository_external_id(data["repository"]["id"]) repository.set_repository_name(full_name) - repository.set_repository_type('github') - repository.set_repository_url(data['repository']['html_url']) + repository.set_repository_type("github") + repository.set_repository_url(data["repository"]["html_url"]) repository.save() return repository except Exception as err: - cla.log.warning('Could not create GitHub repository automatically: %s', str(err)) + cla.log.warning("Could not create GitHub repository automatically: %s", str(err)) return None -def handle_commit_from_user(project, commit_sha, author_info, signed, missing): # pylint: disable=too-many-arguments +def handle_commit_from_user( + project, user_commit_summary: UserCommitSummary, signed: List[UserCommitSummary], missing: List[UserCommitSummary] +): # pylint: disable=too-many-arguments """ Helper method to triage commits between signed and not-signed user signatures. - :param project: The project model for this github PR organization. - :type project: Project - :param commit_sha: Commit has as a string - :type commit_sha: string - :param author_info: the commit author details, including id, name, email (if available) - :type author_info: tuple of (author_id, author_username, author_email) - :param signed: Reference to a list of signed authors so far. Should be modified - in-place to add a signer if found. - :type signed: list of strings - :param missing: Reference to a list of authors who have not signed yet. - Should be modified in-place to add a missing signer if found. - :type missing: list of strings + :param: project: The project model for this GitHub PR organization. + :type: project: Project + :param: user_commit_summary: a user commit summary object + :type: UserCommitSummary + :param signed: A list of authors who have signed. + Should be modified in-place to add the signer information. + :type: List[UserCommitSummary] + :param missing: A list of authors who have not signed yet. + Should be modified in-place to add the missing signer information. + :type: List[UserCommitSummary] """ - # Extract the author_info tuple details - author_id = author_info[0] - author_username = author_info[1] - author_email = author_info[2] - cla.log.debug(f'Looking up GitHub user (author_id: {author_id}, ' - f'author_username: {author_username}, ' - f'auth_email: {author_email})') + fn = "cla.models.github_models.handle_commit_from_user" + # handle edge case of non existant users + if not user_commit_summary.is_valid_user(): + missing.append(user_commit_summary) + return # attempt to lookup the user in our database by GH id - # may return multiple users that match this author_id - users = cla.utils.get_user_instance().get_user_by_github_id(author_id) + users = cla.utils.get_user_instance().get_user_by_github_id(user_commit_summary.author_id) if users is None: # GitHub user not in system yet, signature does not exist for this user. - cla.log.debug(f'GitHub user (id: {author_id}, ' - f'user: {author_username}, ' - f'email: {author_email}) lookup by github id not found in our database, ' - 'attempting to looking up user by email...') + cla.log.debug( + f"{fn} - User commit summary: {user_commit_summary} " + f"lookup by github numeric id not found in our database, " + "attempting to looking up user by email..." + ) # Try looking up user by email as a fallback - users = cla.utils.get_user_instance().get_user_by_email(author_email) - - # Got one or more records by searching the email + users = cla.utils.get_user_instance().get_user_by_email(user_commit_summary.author_email) + if users is None: + # Try looking up user by github username + cla.log.debug( + f"{fn} - User commit summary: {user_commit_summary} " + f"lookup by github email not found in our database, " + "attempting to looking up user by github username..." + ) + users = cla.utils.get_user_instance().get_user_by_github_username(user_commit_summary.author_login) + + # Got one or more records by searching the email or username if users is not None: - cla.log.debug(f'Found {len(users)} GitHub user(s) matching github email: {author_email}') + cla.log.debug( + f"{fn} - Found {len(users)} GitHub user(s) matching " f"github email: {user_commit_summary.author_email}" + ) + for user in users: - cla.log.debug(f'GitHub user found in our database: {user}') + cla.log.debug(f"{fn} - GitHub user found in our database: {user}") # For now, accept non-github users as legitimate users. # Does this user have a signed signature for this project? If so, add to the signed list and return, # no reason to continue looking if cla.utils.user_signed_project_signature(user, project): - signed.append((commit_sha, author_username)) + user_commit_summary.authorized = True + signed.append(user_commit_summary) return # Didn't find a signed signature for this project - add to our missing bucket list - # author_info consists of: [author_id, author_username, author_email] - missing.append((commit_sha, list(author_info))) + # author_info consists of: [author_id, author_login, author_username, author_email] + missing.append(user_commit_summary) else: # Not seen this user before - no record on file in our user's database - cla.log.debug(f'GitHub user (id: {author_id}, ' - f'user: {author_username}, ' - f'email: {author_email}) lookup by email in our database failed - not found') + cla.log.debug( + f"{fn} - User commit summary: {user_commit_summary} " f"lookup by email in our database failed - not found" + ) # This bit of logic below needs to be reconsidered - query logic takes a very long time for large # projects like CNCF which significantly delays updating the GH PR status. @@ -773,69 +1251,189 @@ def handle_commit_from_user(project, commit_sha, author_info, signed, missing): # For now - we'll just return the author info as a list without the flag to indicate that they have been on # the approved list for any company/signature - # author_info consists of: [author_id, author_username, author_email] - missing.append((commit_sha, list(author_info))) + # author_info consists of: [author_id, author_login, author_username, author_email] + missing.append(user_commit_summary) else: - cla.log.debug(f'Found {len(users)} GitHub user(s) matching github id: {author_id} in our database') + cla.log.debug( + f"{fn} - Found {len(users)} GitHub user(s) matching " + f"github id: {user_commit_summary.author_id} in our database" + ) if len(users) > 1: - cla.log.warning(f'more than 1 user found in our user database - user: {users} - ' - f'will ONLY evaluate the first one') + cla.log.warning( + f"{fn} - more than 1 user found in our user database - user: {users} - " f"will ONLY evaluate the first one" + ) # Just review the first user that we were able to fetch from our DB user = users[0] - cla.log.debug(f'GitHub user found in our database: {user}') + cla.log.debug(f"{fn} - GitHub user found in our database: {user}") # Does this user have a signed signature for this project? If so, add to the signed list and return, # no reason to continue looking if cla.utils.user_signed_project_signature(user, project): - signed.append((commit_sha, author_username)) + user_commit_summary.authorized = True + signed.append(user_commit_summary) return - list_author_info = list(author_info) - # If the user does not have a company ID assigned, then they have not been associated with a company as # part of the Contributor console workflow if user.get_user_company_id() is None: - missing.append((commit_sha, list_author_info)) + # User is not affiliated with a company + missing.append(user_commit_summary) return + # Mark the user as having a company affiliation + user_commit_summary.affiliated = True + # Perform a specific search for the user's project + company + CCLA signatures = cla.utils.get_signature_instance().get_signatures_by_project( project_id=project.get_project_id(), signature_signed=True, signature_approved=True, - signature_type='ccla', - signature_reference_type='company', + signature_type="ccla", + signature_reference_type="company", signature_reference_id=user.get_user_company_id(), signature_user_ccla_company_id=None, ) # Should only return one signature record - cla.log.debug(f'Found {len(signatures)} CCLA signatures for company: {user.get_user_company_id()}, ' - f'project: {project.get_project_id()} in our database.') + cla.log.debug( + f"{fn} - Found {len(signatures)} CCLA signatures for company: {user.get_user_company_id()}, " + f"project: {project.get_project_id()} in our database." + ) # Should never happen - warn if we see this if len(signatures) > 1: - cla.log.warning(f'more than 1 CCLA signature record found in our database - signatures: {signatures}') + cla.log.warning(f"{fn} - more than 1 CCLA signature record found in our database - signatures: {signatures}") for signature in signatures: if cla.utils.is_approved( - signature, - email=author_email, - github_id=author_id, - github_username=author_username + signature, + email=user_commit_summary.author_email, + github_id=user_commit_summary.author_id, + github_username=user_commit_summary.author_login, # double check this... ): - # Append whitelisted flag to the author info list - cla.log.debug(f'Github user(id:{author_id}, ' - f'user: {author_username}, ' - f'email {author_email}) is on the approved list, ' - 'but not affiliated with a company') - list_author_info.append(True) + cla.log.debug( + f"{fn} - User Commit Summary: {user_commit_summary}, " + "is on one of the approval lists, but not affiliated with a company" + ) + user_commit_summary.authorized = True break - missing.append((commit_sha, list_author_info)) + missing.append(user_commit_summary) + + +def get_merge_group_commit_authors(merge_group_sha, installation_id=None) -> List[UserCommitSummary]: + """ + Helper function to extract all committer information for a GitHub merge group. + + :param: merge_group_sha: A GitHub merge group sha to examine. + :type: merge_group_sha: string + :return: A list of User Commit Summary objects containing the commit sha and available user information + """ + + fn = "cla.models.github_models.get_merge_group_commit_authors" + cla.log.debug(f"Querying merge group {merge_group_sha} for commit authors...") + commit_authors = [] + try: + g = cla.utils.get_github_integration_instance(installation_id=installation_id) + commit = g.get_commit(merge_group_sha) + for parent in commit.parents: + try: + cla.log.debug(f"{fn} - Querying parent commit {parent.sha} for commit authors...") + commit = g.get_commit(parent.sha) + cla.log.debug(f"{fn} - Found {commit.commit.author.name} as the author of parent commit {parent.sha}") + commit_authors.append( + UserCommitSummary( + parent.sha, + commit.author.id, + commit.author.login, + commit.author.name, + commit.author.email, + False, + False, + ) + ) + except (GithubException, IncompletableObject) as e: + cla.log.warning(f"{fn} - Unable to query parent commit {parent.sha} for commit authors: {e}") + commit_authors.append(UserCommitSummary(parent.sha, None, None, None, None, False, False)) + + except Exception as e: + cla.log.warning(f"{fn} - Unable to query merge group {merge_group_sha} for commit authors: {e}") + + return commit_authors + + +def get_author_summary(commit, pr, installation_id) -> List[UserCommitSummary]: + """ + Helper function to extract author information from a GitHub commit. + :param commit: A GitHub commit object. + :type commit: github.Commit.Commit + :param pr: PR number + :type pr: int + """ + fn = "cla.models.github_models.get_author_summary" + commit_authors = [] + if commit.author: + try: + commit_author_summary = UserCommitSummary( + commit.sha, + commit.author.id, + commit.author.login, + commit.author.name, + commit.author.email, + False, + False, # default not authorized - will be evaluated and updated later + ) + cla.log.debug(f"{fn} - PR: {pr}, {commit_author_summary}") + # check for co-author details + # issue # 3884 + commit_authors.append(commit_author_summary) + # co_authors = cla.utils.get_co_authors_from_commit(commit) + # with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + # for co_author in co_authors: + # commit_authors.append( + # executor.submit(get_co_author_commits, co_author, commit, pr, installation_id).result() + # ) + + return commit_authors + except (GithubException, IncompletableObject) as exc: + cla.log.warning(f"{fn} - PR: {pr}, unable to get commit author summary: {exc}") + try: + # commit.commit.author is a github.GitAuthor.GitAuthor object type - object + # only has date, name and email attributes - no ID attribute/value + # https://pygithub.readthedocs.io/en/latest/github_objects/GitAuthor.html + commit_author_summary = UserCommitSummary( + commit.sha, + None, + None, + commit.commit.author.name, + commit.commit.author.email, + False, + False, # default not authorized - will be evaluated and updated later + ) + cla.log.debug(f"{fn} - github.GitAuthor.GitAuthor object: {commit.commit.author}") + cla.log.debug( + f"{fn} - PR: {pr}, " + f"GitHub NamedUser author NOT found for commit SHA {commit_author_summary} " + f"however, we did find GitAuthor info" + ) + cla.log.debug(f"{fn} - PR: {pr}, {commit_author_summary}") + commit_authors.append(commit_author_summary) + return commit_authors + except (GithubException, IncompletableObject) as exc: + cla.log.warning(f"{fn} - PR: {pr}, unable to get commit author summary: {exc}") + commit_author_summary = UserCommitSummary(commit.sha, None, None, None, None, False, False) + cla.log.warning(f"{fn} - PR: {pr}, " f"could not find any commit author for SHA {commit_author_summary}") + commit_authors.append(commit_author_summary) + return commit_authors + else: + cla.log.warning(f"{fn} - PR: {pr}, " f"could not find any commit author for SHA {commit.sha}") + commit_author_summary = UserCommitSummary(commit.sha, None, None, None, None, False, False) + commit_authors.append(commit_author_summary) + return commit_authors -def get_pull_request_commit_authors(pull_request): + +def get_pull_request_commit_authors(pull_request, installation_id) -> List[UserCommitSummary]: """ Helper function to extract all committer information for a GitHub PR. @@ -846,61 +1444,77 @@ def get_pull_request_commit_authors(pull_request): For activity callback, see: https://developer.github.com/v3/activity/events/types/#pullrequestevent - :param pull_request: A GitHub pull request to examine. - :type pull_request: GitHub.PullRequest - :return: A list of tuples containing a tuple of (commit_sha_string, (author_id, author_username, author_email)) - - the second item is another tuple of author info. - :rtype: [(commit_sha_string, (author_id, author_username, author_email)] + :param: pull_request: A GitHub pull request to examine. + :type: pull_request: GitHub.PullRequest + :return: A list of User Commit Summary objects containing the commit sha and available user information + :rtype: List[UserCommitSummary] """ - cla.log.debug('Querying pull request commits for author information...') + fn = "cla.models.github_models.get_pull_request_commit_authors" + cla.log.debug(f"{fn} - Querying pull request commits for author information...") + no_commits = pull_request.get_commits().totalCount + cla.log.debug(f"{fn} - PR: {pull_request.number}, number of commits: {no_commits}") + commit_authors = [] - for commit in pull_request.get_commits(): - cla.log.debug('Processing commit while looking for authors, commit: {}'.format(commit.sha)) - # Note: we can get the author info in two different ways: - if commit.author is not None: - # commit.author is a github.NamedUser.NamedUser type object - # https://pygithub.readthedocs.io/en/latest/github_objects/NamedUser.html - if commit.author.name is not None: - cla.log.debug('PR: {}, GitHub commit.author.name author found for commit SHA {}, ' - 'author id: {}, name: {}, email: {}'. - format(pull_request.number, commit.sha, commit.author.id, - commit.author.name, commit.author.email)) - commit_authors.append((commit.sha, (commit.author.id, commit.author.name, commit.author.email))) - elif commit.author.login is not None: - cla.log.debug('PR: {}, GitHub commit.author.login author found for commit SHA {}, ' - 'author id: {}, login: {}, email: {}'. - format(pull_request.number, commit.sha, commit.author.id, - commit.author.login, commit.author.email)) - commit_authors.append((commit.sha, (commit.author.id, commit.author.login, commit.author.email))) - else: - cla.log.debug(f'PR: {pull_request.number}, GitHub commit.author.name and commit.author.login ' - f'author information NOT found for commit SHA {commit.sha}, ' - f'author id: {commit.author.id}, ' - f'name: {commit.author.name}, ' - f'login: {commit.author.login}, ' - f'email: {commit.author.email}') - commit_authors.append((commit.sha, None)) - elif commit.commit.author is not None: - cla.log.debug('github.GitAuthor.GitAuthor object: {}'.format(commit.commit.author)) - # commit.commit.author is a github.GitAuthor.GitAuthor object type - object - # only has date, name and email attributes - no ID attribute/value - # https://pygithub.readthedocs.io/en/latest/github_objects/GitAuthor.html - cla.log.debug('PR: {}, GitHub NamedUser author NOT found for commit SHA {}, ' - 'however, found GitAuthor author id: None, name: {}, email: {}'. - format(pull_request.number, commit.sha, - commit.commit.author.name, commit.commit.author.email)) - commit_authors.append((commit.sha, (None, commit.commit.author.name, commit.commit.author.email))) - else: - cla.log.warning('PR: {}, could not find any commit author for SHA {}'. - format(pull_request.number, commit.sha)) - commit_authors.append((commit.sha, None)) + + with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: + future_to_commit = { + executor.submit(get_author_summary, commit, pull_request.number, installation_id): commit + for commit in pull_request.get_commits() + } + for future in concurrent.futures.as_completed(future_to_commit): + future_to_commit[future] + try: + commit_authors.extend(future.result()) + except Exception as exc: + cla.log.warning(f"{fn} - PR: {pull_request.number}, get_author_summary generated an exception: {exc}") + raise exc return commit_authors -def has_check_previously_failed(pull_request: PullRequest): +def get_co_author_commits(co_author, commit, pr, installation_id): + fn = "cla.models.github_models.get_co_author_commits" + # check if co-author is a github user + co_author_summary = None + login, github_id = None, None + email = co_author[1] + name = co_author[0] + # get repository service + github = cla.utils.get_repository_service("github") + cla.log.debug(f"{fn} - getting co-author details: {co_author}, email: {email}, name: {name}") + try: + user = github.get_github_user_by_email(email, installation_id) + except (GithubException, IncompletableObject, RateLimitExceededException) as ex: + # user not found + cla.log.debug(f"{fn} - co-author github user not found : {co_author} with exception: {ex}") + user = None + cla.log.debug(f"{fn} - co-author: {co_author}, user: {user}") + if user: + cla.log.debug(f"{fn} - co-author github user details found : {co_author}, user: {user}") + login = user.login + github_id = user.id + co_author_summary = UserCommitSummary( + commit.sha, + github_id, + login, + name, + email, + False, + False, # default not authorized - will be evaluated and updated later + ) + cla.log.debug(f"{fn} - PR: {pr}, {co_author_summary}") + else: + co_author_summary = UserCommitSummary( + commit.sha, None, None, name, email, False, False # default not authorized - will be evaluated and updated later + ) + cla.log.debug(f"{fn} - co-author github user details not found : {co_author}") + + return co_author_summary + + +def has_check_previously_passed_or_failed(pull_request: PullRequest): """ - Review the status updates in the PR. Identify 1 or more previous failed + Review the status updates in the PR. Identify 1 or more previous failed|passed updates from the EasyCLA bot. If we fine one, return True with the comment, otherwise return False, None @@ -912,39 +1526,49 @@ def has_check_previously_failed(pull_request: PullRequest): for comment in comments: # Our bot comments include the following text # A previously failed check has 'not authorized' somewhere in the body - if 'is not authorized under a signed CLA' in comment.body: + if "is not authorized under a signed CLA" in comment.body: + return True, comment + if "they must confirm their affiliation" in comment.body: + return True, comment + if "is missing the User" in comment.body: return True, comment - if 'they must confirm their affiliation' in comment.body: + if "are authorized under a signed CLA" in comment.body: return True, comment - if 'CLA Missing ID' in comment.body and 'is missing the User' in comment.body: + if "is not linked to the GitHub account" in comment.body: return True, comment return False, None -def update_pull_request(installation_id, github_repository_id, pull_request, repository_name, signed, - missing, project_version): # pylint: disable=too-many-locals +def update_pull_request( + installation_id, + github_repository_id, + pull_request, + repository_name, + signed: List[UserCommitSummary], + missing: List[UserCommitSummary], + project_version, +): # pylint: disable=too-many-locals """ Helper function to update a PR's comment and status based on the list of signers. - :param installation_id: The ID of the GitHub installation - :type installation_id: int - :param github_repository_id: The ID of the GitHub repository this PR belongs to. - :type github_repository_id: int - :param pull_request: The GitHub PullRequest object for this PR. - :type pull_request: GitHub.PullRequest - :param repository_name: The GitHub repository name for this PR. - :type repository_name: string - :param signed: The list of (commit hash, author name) tuples that have signed an - signature for this PR. - :type signed: [(string, string)] - :param missing: The list of (commit hash, author name) tuples that have not signed - an signature for this PR. - :type missing: [(string, list)] - :param project_version: Project version associated with PR - :type missing: string + :param: installation_id: The ID of the GitHub installation + :type: installation_id: int + :param: github_repository_id: The ID of the GitHub repository this PR belongs to. + :type: github_repository_id: int + :param: pull_request: The GitHub PullRequest object for this PR. + :type: pull_request: GitHub.PullRequest + :param: repository_name: The GitHub repository name for this PR. + :type: repository_name: string + :param: signed: The list of User Commit Summary objects for this PR. + :type: signed: List[UserCommitSummary] + :param: missing: The list of User Commit Summary objects for this PR. + :type: missing: List[UserCommitSummary] + :param: project_version: Project version associated with PR + :type: missing: string """ - notification = cla.conf['GITHUB_PR_NOTIFICATION'] - both = notification == 'status+comment' or notification == 'comment+status' + fn = "cla.models.github_models.update_pull_request" + notification = cla.conf["GITHUB_PR_NOTIFICATION"] + both = notification == "status+comment" or notification == "comment+status" last_commit = pull_request.get_commits().reversed[0] # Here we update the PR status by adding/updating the PR body - this is the way the EasyCLA app @@ -952,27 +1576,23 @@ def update_pull_request(installation_id, github_repository_id, pull_request, rep # Create check run for users that haven't yet signed and/or affiliated if missing: text = "" - for authors in missing: - # Check for valid github id - if authors[1][0] is None: + help_url = "" + + for user_commit_summary in missing: + # Check for valid GitHub id + # old tuple: (sha, (author_id, author_login_or_name, author_email, optionalTrue)) + if not user_commit_summary.is_valid_user(): help_url = "https://help.github.com/en/github/committing-changes-to-your-project/why-are-my-commits-linked-to-the-wrong-user" else: - help_url = cla.utils.get_full_sign_url('github', str(installation_id), github_repository_id, - pull_request.number, project_version) - client = GitHubInstallation(installation_id) + help_url = cla.utils.get_full_sign_url( + "github", str(installation_id), github_repository_id, pull_request.number, project_version + ) + # check if unsigned user is whitelisted - commit_sha = authors[0] - if commit_sha != last_commit.sha: + if user_commit_summary.commit_sha != last_commit.sha: continue - author_email = authors[1][2] - author_id = authors[1][0] - if author_id: - if len(authors[1]) == 4: - text += f'{author_email} must confirm corporate affiliation.\n' - else: - text += f'{author_email} is not authorized under a signed CLA.\n' - else: - text += f'{author_email} is not linked to this commit. \n' + + text += user_commit_summary.get_display_text(tag_user=True) payload = { "name": "CLA check", @@ -986,71 +1606,115 @@ def update_pull_request(installation_id, github_repository_id, pull_request, rep "text": text, }, } + client = GitHubInstallation(installation_id) client.create_check_run(repository_name, json.dumps(payload)) # Update the comment - if both or notification == 'comment': - body = cla.utils.assemble_cla_comment('github', str(installation_id), github_repository_id, pull_request.number, - signed, missing, project_version) - previously_failed, comment = has_check_previously_failed(pull_request) + if both or notification == "comment": + body = cla.utils.assemble_cla_comment( + "github", str(installation_id), github_repository_id, pull_request.number, signed, missing, project_version + ) + previously_pass_or_failed, comment = has_check_previously_passed_or_failed(pull_request) if not missing: # After Issue #167 wsa in place, they decided via Issue #289 that we # DO want to update the comment, but only after we've previously failed - if previously_failed: - cla.log.debug('Found previously failed checks - updating CLA comment in PR.') + if previously_pass_or_failed: + cla.log.debug(f"{fn} - Found previously passed or failed checks - updating CLA comment in PR.") comment.edit(body) - cla.log.debug('EasyCLA App checks pass for PR: {} with authors: {}'.format(pull_request.number, signed)) + cla.log.debug(f"{fn} - EasyCLA App checks pass for PR: {pull_request.number} with authors: {signed}") else: # Per Issue #167, only add a comment if check fails # update_cla_comment(pull_request, body) - if previously_failed: - cla.log.debug('Found previously failed checks - updating CLA comment in PR.') + if previously_pass_or_failed: + cla.log.debug(f"{fn} - Found previously failed checks - updating CLA comment in PR.") comment.edit(body) else: pull_request.create_issue_comment(body) - cla.log.debug('EasyCLA App checks fail for PR: {}. CLA signatures with signed authors: {} and ' - 'with missing authors: {}'.format(pull_request.number, signed, missing)) + cla.log.debug( + f"{fn} - EasyCLA App checks fail for PR: {pull_request.number}. " + f"CLA signatures with signed authors: {signed} and " + f"with missing authors: {missing}" + ) - if both or notification == 'status': - context_name = os.environ.get('GH_STATUS_CTX_NAME') + if both or notification == "status": + context_name = os.environ.get("GH_STATUS_CTX_NAME") if context_name is None: - context_name = 'communitybridge/cla' + context_name = "communitybridge/cla" # if we have ANY committers who have failed the check - update the status with overall failure if missing is not None and len(missing) > 0: - state = 'failure' + state = "failure" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=False) sign_url = cla.utils.get_full_sign_url( - 'github', str(installation_id), github_repository_id, pull_request.number, project_version) - cla.log.debug(f'Creating new CLA {state} status - {len(signed)} passed, {missing}, signing url: {sign_url}') + "github", str(installation_id), github_repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) create_commit_status(pull_request, last_commit.sha, state, sign_url, body, context) elif signed is not None and len(signed) > 0: - state = 'success' + state = "success" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=True) sign_url = cla.conf["CLA_LANDING_PAGE"] # Remove this once signature detail page ready. + sign_url = os.path.join(sign_url, "#/") sign_url = append_project_version_to_url(address=sign_url, project_version=project_version) - cla.log.debug(f'Creating new CLA {state} status - {len(signed)} passed, {missing}, signing url: {sign_url}') + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) create_commit_status(pull_request, last_commit.sha, state, sign_url, body, context) else: - # error condition - should have a least one committer and they would be in one of the above + # error condition - should have at least one committer, and they would be in one of the above # lists: missing or signed - state = 'failure' + state = "failure" # For status, we change the context from author_name to 'communitybridge/cla' or the # specified default value per issue #166 context, body = cla.utils.assemble_cla_status(context_name, signed=False) sign_url = cla.utils.get_full_sign_url( - 'github', str(installation_id), github_repository_id, pull_request.number, project_version) - cla.log.debug(f'Creating new CLA {state} status - {len(signed)} passed, {missing}, signing url: {sign_url}') - cla.log.warning('This is an error condition - should have at least one committer in one of these lists: ' - f'{len(signed)} passed, {missing}') + "github", str(installation_id), github_repository_id, pull_request.number, project_version + ) + cla.log.debug( + f"{fn} - Creating new CLA '{state}' status - {len(signed)} passed, {missing} failed, " + f"signing url: {sign_url}" + ) + cla.log.warning( + "{fn} - This is an error condition - " + f"should have at least one committer in one of these lists: " + f"{len(signed)} passed, {missing}" + ) create_commit_status(pull_request, last_commit.sha, state, sign_url, body, context) +def create_commit_status_for_merge_group(commit_obj, merge_commit_sha, state, sign_url, body, context): + """ + Helper function to create a pull request commit status message. + + :param commit_obj: The commit object to post a status on. + :type commit_obj: Commit + :param merge_commit_sha: The commit hash to post a status on. + :type merge_commit_sha: string + :param state: The state of the status. + :type state: string + :param sign_url: The link the user will be taken to when clicking on the status message. + :type sign_url: string + :param body: The contents of the status message. + :type body: string + """ + try: + # Create status + cla.log.debug(f"Creating commit status for merge commit {merge_commit_sha}") + commit_obj.create_status(state=state, target_url=sign_url, description=body, context=context) + + except Exception as e: + cla.log.warning(f"Unable to create commit status for " f"and merge commit {merge_commit_sha}: {e}") + + def create_commit_status(pull_request, commit_hash, state, sign_url, body, context): """ Helper function to create a pull request commit status message given the PR and commit hash. @@ -1073,20 +1737,26 @@ def create_commit_status(pull_request, commit_hash, state, sign_url, body, conte commit_obj = commit break if commit_obj is None: - cla.log.error(f'Could not post status {state} on ' - f'PR: {pull_request.number}, ' - f'Commit: {commit_hash} not found') + cla.log.error( + f"Could not post status {state} on " f"PR: {pull_request.number}, " f"Commit: {commit_hash} not found" + ) return # context is a string label to differentiate one signer status from another signer status. # committer name is used as context label - commit_obj.create_status(state, sign_url, body, context) - cla.log.info(f'Successfully posted status {state} on PR {pull_request.number}: Commit {commit_hash}' - f'with SignUrl : {sign_url}') + cla.log.info(f"Updating status with state '{state}' on PR {pull_request.number} for commit {commit_hash}...") + # returns github.CommitStatus.CommitStatus + resp = commit_obj.create_status(state, sign_url, body, context) + cla.log.info( + f"Successfully posted status '{state}' on PR {pull_request.number}: Commit {commit_hash} " + f"with SignUrl : {sign_url} with response: {resp}" + ) except GithubException as exc: - cla.log.error(f'Could not post status {state} on PR: {pull_request.number}, ' - f'Commit: {commit_hash}, ' - f'Response Code: {exc.status}, ' - f'Message: {exc.data}') + cla.log.error( + f"Could not post status '{state}' on PR: {pull_request.number}, " + f"Commit: {commit_hash}, " + f"Response Code: {exc.status}, " + f"Message: {exc.data}" + ) # def update_cla_comment(pull_request, body): @@ -1148,26 +1818,28 @@ def _get_github_client(self, username, token): return MockGitHubClient(username, token) def _get_authorization_url_and_state(self, client_id, redirect_uri, scope, authorize_url): - authorization_url = 'http://authorization.url' - state = 'random-state-here' + authorization_url = "http://authorization.url" + state = "random-state-here" return authorization_url, state def _fetch_token(self, client_id, state, token_url, client_secret, code): # pylint: disable=too-many-arguments - return 'random-token' + return "random-token" - def _get_request_session(self, request): + def _get_request_session(self, request) -> dict: if self.oauth2_token: - return {'github_oauth2_token': 'random-token', - 'github_oauth2_state': 'random-state', - 'github_origin_url': 'http://github/origin/url', - 'github_installation_id': 1} + return { + "github_oauth2_token": "random-token", + "github_oauth2_state": "random-state", + "github_origin_url": "http://github/origin/url", + "github_installation_id": 1, + } return {} - def get_user_data(self, session, client_id): - return {'email': 'test@user.com', 'name': 'Test User', 'id': 123} + def get_user_data(self, session, client_id) -> dict: + return {"email": "test@user.com", "name": "Test User", "id": 123} def get_user_emails(self, session, client_id): - return [{'email': 'test@user.com', 'verified': True, 'primary': True, 'visibility': 'public'}] + return [{"email": "test@user.com", "verified": True, "primary": True, "visibility": "public"}] def get_pull_request(self, github_repository_id, pull_request_number, installation_id): return MockGitHubPullRequest(pull_request_number) @@ -1211,7 +1883,7 @@ class MockGitHubPullRequest(object): # pylint: disable=too-few-public-methods def __init__(self, pull_request_id): self.number = pull_request_id - self.html_url = 'http://test-github.com/user/repo/' + str(self.number) + self.html_url = "http://test-github.com/user/repo/" + str(self.number) def get_commits(self): # pylint: disable=no-self-use """ @@ -1238,7 +1910,8 @@ class MockGitHubComment(object): # pylint: disable=too-few-public-methods """ A GitHub mock issue comment object for testing. """ - body = 'Test' + + body = "Test" class MockPaginatedList(github.PaginatedList.PaginatedListBase): # pylint: disable=too-few-public-methods @@ -1267,7 +1940,7 @@ class MockGitHubCommit(object): # pylint: disable=too-few-public-methods def __init__(self): self.author = MockGitHubAuthor() - self.sha = 'sha-test-commit' + self.sha = "sha-test-commit" def create_status(self, state, sign_url, body): """ @@ -1283,5 +1956,5 @@ class MockGitHubAuthor(object): # pylint: disable=too-few-public-methods def __init__(self, author_id=1): self.id = author_id - self.login = 'user' - self.email = 'user@github.com' + self.login = "user" + self.email = "user@github.com" diff --git a/cla-backend/cla/models/model_interfaces.py b/cla-backend/cla/models/model_interfaces.py index 49665dacc..9c7d0db86 100644 --- a/cla-backend/cla/models/model_interfaces.py +++ b/cla-backend/cla/models/model_interfaces.py @@ -937,9 +937,18 @@ def get_github_whitelist(self): def get_github_org_whitelist(self): raise NotImplementedError() + def get_gitlab_org_approval_list(self): + raise NotImplementedError + + def get_gitlab_username_approval_list(self): + raise NotImplementedError + def get_note(self): raise NotImplementedError() + def get_auto_create_ecla(self): + raise NotImplementedError() + def set_signature_id(self, signature_id): """ Setter for an signature ID. @@ -1788,6 +1797,52 @@ def set_document_tab_height(self, tab_height): raise NotImplementedError() +class GitlabOrg(object): + """ + Interface to the GitlabOrg model + """ + + def to_dict(self): + """ + Converts models to dictionaries for JSON serialization. + + :return: A dict representation of the model. + :rtype: dict + """ + raise NotImplementedError() + + def save(self): + """ + Simple abstraction around the supported ORMs to save a model. + """ + raise NotImplementedError() + + def load(self, organization_id): + """ + Simple abstraction around the supported ORMs to load a model. + Should populate the current object. + + :param organization_id: The gitlab organization's ID. + :type organization_id: string + """ + raise NotImplementedError() + + def delete(self): + """ + Simple abstraction around the supported ORMs to delete a model. + """ + raise NotImplementedError() + + def all(self): + """ + Fetches all gitlab organizations in the CLA system. + + :return: A list of GitlabOrg objects. + :rtype: [cla.models.model_interfaces.GitlabOrg] + """ + raise NotImplementedError() + + class GitHubOrg(object): """ Interface to the GitHubOrg model. diff --git a/cla-backend/cla/models/model_utils.py b/cla-backend/cla/models/model_utils.py new file mode 100644 index 000000000..c3d1c6a69 --- /dev/null +++ b/cla-backend/cla/models/model_utils.py @@ -0,0 +1,24 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +""" +Utility functions for the models +""" +from uuid import UUID + + +def is_uuidv4(uuid_string: str) -> bool: + """ + Helper function for determining if the specified string is a UUID v4 value. + :param uuid_string: the string representing a UUID + :return: True if the specified string is a UUID v4 value, False otherwise + """ + try: + UUID(uuid_string, version=4) + return True + except TypeError: + # If it's a value error, then the string is not a valid UUID. + return False + except ValueError: + # If it's a value error, then the string is not a valid UUID. + return False diff --git a/cla-backend/cla/project_service.py b/cla-backend/cla/project_service.py index dd9fa3895..9a012cb74 100644 --- a/cla-backend/cla/project_service.py +++ b/cla-backend/cla/project_service.py @@ -3,12 +3,13 @@ import datetime import json import os +from typing import Optional import requests import cla from cla import log -from cla.config import THE_LINUX_FOUNDATION +from cla.config import THE_LINUX_FOUNDATION, LF_PROJECTS_LLC STAGE = os.environ.get('STAGE', '') REGION = 'us-east-1' @@ -35,9 +36,12 @@ def is_standalone(self, project_sfid) -> bool: """ project = self.get_project_by_id(project_sfid) if project: - parent_sf_id = project.get('Parent', None) - if not self.has_parent(project) and (parent_sf_id is None or parent_sf_id == THE_LINUX_FOUNDATION): + parent_name = self.get_parent_name(project) + if parent_name is None or (parent_name == THE_LINUX_FOUNDATION or parent_name == LF_PROJECTS_LLC) \ + and not project.get('Projects'): return True + else: + return False return False def is_lf_supported(self, project_sfid) -> bool: @@ -50,9 +54,10 @@ def is_lf_supported(self, project_sfid) -> bool: """ project = self.get_project_by_id(project_sfid) if project: + parent_name = self.get_parent_name(project) return (project.get('Funding', None) == 'Unfunded' or project.get('Funding', None) == 'Supported By Parent') and \ - project.get('Parent', None) == THE_LINUX_FOUNDATION + (parent_name == THE_LINUX_FOUNDATION or parent_name == LF_PROJECTS_LLC) return False def has_parent(self, project) -> bool: @@ -60,14 +65,25 @@ def has_parent(self, project) -> bool: fn = 'project_service.has_parent' try: log.info(f"{fn} - Checking if {project['Name']} has parent project") - parent = project['Parent'] - if parent: + if project and project['Foundation']['ID'] != '' and project['Foundation']['Name'] != '': return True except KeyError as err: - log.debug(f"{fn} - Failed to find parent for {project['Name']} , error: {err}") + log.debug(f"{fn} - Failed to find parent for {project['Name']}, error: {err}") return False return False + def get_parent_name(self, project) -> Optional[str]: + """ returns the project parent name if exists, otherwise returns None """ + fn = 'project_service.get_parent_name' + try: + log.info(f"{fn} - Checking if {project['Name']} has parent project") + if project and project['Foundation']['ID'] != '' and project['Foundation']['Name'] != '': + return project['Foundation']['Name'] + except KeyError as err: + log.debug(f"{fn} - Failed to find parent for {project['Name']}, error: {err}") + return None + return None + def is_parent(self, project) -> bool: """ checks whether salesforce project is a parent diff --git a/cla-backend/cla/routes.py b/cla-backend/cla/routes.py index 351e1ba1c..cdb80a37a 100755 --- a/cla-backend/cla/routes.py +++ b/cla-backend/cla/routes.py @@ -34,6 +34,7 @@ get_supported_repository_providers, get_supported_document_content_types, get_session_middleware, + get_log_middleware ) @@ -97,20 +98,20 @@ def get_health(request): @hug.get("/user/{user_id}", versions=2) -def get_user(request, user_id: hug.types.uuid): +def get_user(user_id: hug.types.uuid): """ GET: /user/{user_id} Returns the requested user data based on ID. """ - try: - auth_user = check_auth(request) - cla.log.debug(f'validated request for: {auth_user}') - except cla.auth.AuthError as auth_err: - if auth_err.response == "missing authorization header": - cla.log.info("getting github user: {}".format(user_id)) - else: - raise auth_err + # try: + # auth_user = check_auth(request) + # cla.log.debug(f'validated request for: {auth_user}') + # except cla.auth.AuthError as auth_err: + # if auth_err.response == "missing authorization header": + # cla.log.info("getting github user: {}".format(user_id)) + # else: + # raise auth_err return cla.controllers.user.get_user(user_id=user_id) @@ -667,6 +668,7 @@ def post_company( auth_user, company_name=company_name, company_manager_id=company_manager_id, + signing_entity_name=company_name, company_manager_user_name=company_manager_user_name, company_manager_user_email=company_manager_user_email, response=response, @@ -815,7 +817,12 @@ def get_project(project_id: hug.types.uuid): cla.log.debug(f'{fn} - querying github repositories for cla group: {project.get("project_name", None)} ' f'with id: {project_id}...') - mapping_record['github_repos'] = Repository().get_repository_by_project_sfid(project_sfid) + repositories = Repository().get_repository_by_project_sfid(project_sfid) + mapping_record['github_repos'] = [] + mapping_record["gitlab_repos"] = [] + if repositories: + mapping_record['github_repos'] = [repo.to_dict() for repo in repositories if repo.get_repository_type() == "github"] + mapping_record["gitlab_repos"] = [repo.to_dict() for repo in repositories if repo.get_repository_type() == "gitlab"] mapping_record['gerrit_repos'] = [] try: cla.log.debug(f'{fn} - querying gerrit repositories for cla group: {project.get("project_name", None)} ' @@ -1196,7 +1203,7 @@ def request_individual_signature( DATA: {'project_id': 'some-project-id', 'user_id': 'some-user-id', - 'return_url_type': Gerrit/Github. Optional depending on presence of return_url + 'return_url_type': Gerrit/Github/GitLab. Optional depending on presence of return_url 'return_url': } Creates a new signature given project and user IDs. The user will be redirected to the @@ -1244,6 +1251,12 @@ def request_corporate_signature( 'authority_email': 'string', 'return_url': } + { + "project_id": "d8cead54-92b7-48c5-a2c8-b1e295e8f7f1", + "company_id": "83f61e34-4457-45a6-a7ab-449ad6efcfbb", + "return_url": "https://corporate.v1.easycla.lfx.linuxfoundation.org/#/company/83f61e34-4457-45a6-a7ab-449ad6efcfbb" + } + Creates a new signature given project and company IDs. The manager will be redirected to the return_url once signature is complete. @@ -1264,14 +1277,14 @@ def request_corporate_signature( # staff_verify(user) or company_manager_verify(user, company_id) return cla.controllers.signing.request_corporate_signature( auth_user=auth_user, - project_id=project_id, - company_id=company_id, + project_id=str(project_id), + company_id=str(company_id), signing_entity_name=signing_entity_name, send_as_email=send_as_email, - authority_name=authority_name, - authority_email=authority_email, - return_url_type=return_url_type, - return_url=return_url, + authority_name=str(authority_name), + authority_email=str(authority_email), + return_url_type=str(return_url_type), + return_url=str(return_url), ) @@ -1340,6 +1353,26 @@ def post_individual_signed( content, installation_id, github_repository_id, change_request_id ) +@hug.post( + "/signed/gitlab/individual/{user_id}/{organization_id}/{gitlab_repository_id}/{merge_request_id}", versions=2, +) +def post_individual_signed_gitlab( + body, + user_id: hug.types.uuid, + organization_id: hug.types.text, + gitlab_repository_id: hug.types.number, + merge_request_id: hug.types.number, +): + """ + POST: /signed/gitlab/individual/{user_id}/{organization_id}/{gitlab_repository_id}/{merge_request_id} + + Callback URL from signing service upon ICLA signature for a Gitlab user. + """ + content = body.read() + return cla.controllers.signing.post_individual_signed_gitlab( + content, user_id, organization_id, gitlab_repository_id, merge_request_id + ) + @hug.post("/signed/gerrit/individual/{user_id}", versions=2) def post_individual_signed_gerrit(body, user_id: hug.types.uuid): @@ -1613,8 +1646,7 @@ def github_app_activity(body, request, response): cla.log.error(f'{fn} - v4 golang api failed with : 500 : {ex}') response.status = HTTP_500 return {"status": f'v4_easycla_github_activity failed {ex}'} - cla.log.debug(f'{fn} - not forwarding event type: \'{event_type}\' with action: \'{action}\'.' - 'Will handle it here...') + cla.log.debug(f'{fn} - not forwarding event type: \'{event_type}\' with action: \'{action}\'.') if event_type is None: cla.log.error(f"{fn} - unable to determine the event type from request headers: {request.headers}") @@ -1779,4 +1811,4 @@ def create_event( # Session Middleware __hug__.http.add_middleware(get_session_middleware()) -__hug__.http.add_middleware(LogMiddleware(logger=cla.log)) +__hug__.http.add_middleware(get_log_middleware()) diff --git a/cla-backend/cla/tests/unit/conftest.py b/cla-backend/cla/tests/unit/conftest.py index 2a70d85d3..efec3c629 100644 --- a/cla-backend/cla/tests/unit/conftest.py +++ b/cla-backend/cla/tests/unit/conftest.py @@ -1,55 +1,43 @@ # Copyright The Linux Foundation and each contributor to CommunityBridge. # SPDX-License-Identifier: MIT -import pytest +from unittest.mock import MagicMock, patch -from unittest.mock import patch, MagicMock -from cla.tests.unit.data import ( - COMPANY_TABLE_DATA, - USER_TABLE_DATA, - SIGNATURE_TABLE_DATA, - EVENT_TABLE_DESCRIPTION, - PROJECT_TABLE_DESCRIPTION, -) -from cla.models.dynamo_models import ( - UserModel, - SignatureModel, - CompanyModel, - EventModel, - ProjectModel, - Signature, - Company, - User, - Project -) +import pytest +from cla.models.dynamo_models import (Company, CompanyModel, EventModel, + Project, ProjectModel, Signature, + SignatureModel, User, UserModel) +from cla.tests.unit.data import (COMPANY_TABLE_DATA, EVENT_TABLE_DESCRIPTION, + PROJECT_TABLE_DESCRIPTION, + SIGNATURE_TABLE_DATA, USER_TABLE_DATA) PATCH_METHOD = "pynamodb.connection.Connection._make_api_call" -@pytest.fixture() -def signature_instance(): - """ - Mock signature instance - """ - with patch(PATCH_METHOD) as req: - req.return_value = SIGNATURE_TABLE_DATA - instance = Signature() - instance.set_signature_id("sig_id") - instance.set_signature_project_id("proj_id") - instance.set_signature_reference_id("ref_id") - instance.set_signature_type("type") - instance.set_signature_project_external_id("proj_id") - instance.set_signature_company_signatory_id("comp_sig_id") - instance.set_signature_company_signatory_name("name") - instance.set_signature_company_signatory_email("email") - instance.set_signature_company_initial_manager_id("manager_id") - instance.set_signature_company_initial_manager_name("manager_name") - instance.set_signature_company_initial_manager_email("manager_email") - instance.set_signature_company_secondary_manager_list({"foo": "bar"}) - instance.set_signature_document_major_version(1) - instance.set_signature_document_minor_version(2) - instance.save() - yield instance +# @pytest.fixture() +# def signature_instance(): +# """ +# Mock signature instance +# """ +# with patch(PATCH_METHOD) as req: +# req.return_value = SIGNATURE_TABLE_DATA +# instance = Signature() +# instance.set_signature_id("sig_id") +# instance.set_signature_project_id("proj_id") +# instance.set_signature_reference_id("ref_id") +# instance.set_signature_type("type") +# instance.set_signature_project_external_id("proj_id") +# instance.set_signature_company_signatory_id("comp_sig_id") +# instance.set_signature_company_signatory_name("name") +# instance.set_signature_company_signatory_email("email") +# instance.set_signature_company_initial_manager_id("manager_id") +# instance.set_signature_company_initial_manager_name("manager_name") +# instance.set_signature_company_initial_manager_email("manager_email") +# instance.set_signature_company_secondary_manager_list({"foo": "bar"}) +# instance.set_signature_document_major_version(1) +# instance.set_signature_document_minor_version(2) +# instance.save() +# yield instance @pytest.fixture() diff --git a/cla-backend/cla/tests/unit/data.py b/cla-backend/cla/tests/unit/data.py index 166401e9c..b70a37556 100644 --- a/cla-backend/cla/tests/unit/data.py +++ b/cla-backend/cla/tests/unit/data.py @@ -48,6 +48,8 @@ {"AttributeName": "signature_company_project_external_id", "AttributeType": "S"}, {"AttributeName": "signature_company_initial_manager_id", "AttributeType": "S"}, {"AttributeName": "signature_company_secondary_manager_list", "AttributeType": "M"}, + {"AttributeName": "signature_reference_id", "AttributeType": "S"}, + {"AttributeName": "signature_project_id", "AttributeType": "S"}, ], "KeySchema": [{"AttributeName": "signature_id", "KeyType": "HASH"}], "GlobalSecondaryIndexes": [ @@ -69,6 +71,13 @@ "Projection": {"ProjectionType": "ALL"}, "ProvisionedThroughput": {"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, }, + { + "IndexName": "signature-project-reference-index", + "KeySchema": [{"AttributeName": "signature_project_id", "KeyType": "HASH"}, + {"AttributeName": "signature_reference_id", "KeyType": "RANGE"}], + "Projection": {"ProjectionType": "ALL"}, + "ProvisionedThroughput": {"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + } ], } } diff --git a/cla-backend/cla/tests/unit/test_company_event.py b/cla-backend/cla/tests/unit/test_company_event.py index ce5c6c1e8..7da712873 100644 --- a/cla-backend/cla/tests/unit/test_company_event.py +++ b/cla-backend/cla/tests/unit/test_company_event.py @@ -3,9 +3,8 @@ from unittest.mock import Mock, patch -import pytest - import cla +import pytest from cla.auth import AuthUser from cla.controllers import company as company_controller from cla.models.dynamo_models import Company diff --git a/cla-backend/cla/tests/unit/test_docusign_models.py b/cla-backend/cla/tests/unit/test_docusign_models.py index 602c7f331..5f05482b8 100644 --- a/cla-backend/cla/tests/unit/test_docusign_models.py +++ b/cla-backend/cla/tests/unit/test_docusign_models.py @@ -3,9 +3,13 @@ import xml.etree.ElementTree as ET -from cla.models.docusign_models import populate_signature_from_ccla_callback, populate_signature_from_icla_callback, \ - create_default_company_values -from cla.models.dynamo_models import Signature, Company +from cla.models.docusign_models import (ClaSignatoryEmailParams, + cla_signatory_email_content, + create_default_company_values, + document_signed_email_content, + populate_signature_from_ccla_callback, + populate_signature_from_icla_callback) +from cla.models.dynamo_models import Company, Project, Signature, User content_icla_agreement_date = """ Hello signatory_name_value,

    " 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'{alt}' + "
    " + ) 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'{alt}' + "" + ) + + if is_approved_by_manager: + badge_url = f"{CLA_LOGO_URL}/cla-confirmation-needed.svg" + alt = "CLA Confirmation Needed" + text = ( + f'{text} ' + f'{alt}' + "" + ) 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 '[![CLA Check](' + badge_url + ')](' + badge_hyperlink + ')' - return (f'' - f'{alt}' - '
    ') + text = ( + f'{text} ' + f'{alt}' + "" + ) + + 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 num_signed > 0: # Group commits by author. committers = {} - for commit, author in signed: - if author is None: - author = "Unknown" - if author not in committers: - committers[author] = [] - committers[author].append(commit) + for user_commit_summary in signed: + if user_commit_summary.is_valid_user(): + author_info = user_commit_summary.get_user_info(tag_user=False) + else: + author_info = "Unknown" + + if author_info not in committers: + committers[author_info] = [] + + # user commit summary includes the author information and the corresponding commit hash + committers[author_info].append(user_commit_summary) + # Print author commit information. - committers_comment += "
        " - for author, commit_hashes in committers.items(): - committers_comment += "
      • " + success + " " + author + " (" + ", ".join(commit_hashes) + ")
      • " - committers_comment += "
      " + for author_info, user_commit_summaries in committers.items(): + # build a quick list of just the commit hash values + commit_shas = [user_commit_summary.commit_sha for user_commit_summary in user_commit_summaries] + cla.log.info(f"{fn} SHAs for signed users: {commit_shas}") + committers_comment += f'
    • {success} {author_info} ({", ".join(commit_shas)})
    • ' if num_missing > 0: support_url = "https://jira.linuxfoundation.org/servicedesk/customer/portal/4" - # Group commits by author. + missing_id_help_url = "https://linuxfoundation.atlassian.net/wiki/spaces/LP/pages/160923756/Missing+ID+on+Commit+but+I+have+an+agreement+on+file" + + # Build a lookup table to group all the commits by author. committers = {} - # Consider the case where github Id does not exist - for commit, author in missing: - if author[0] is None: - author[1] = "Unknown" - if author[1] not in committers: - committers[author[1]] = [] - committers[author[1]].append(commit) - # Check case for whitelisted unsigned user - if len(author) == 4: - committers[author[1]].append(True) + for user_commit_summary in missing: + if user_commit_summary.is_valid_user(): + author_info = user_commit_summary.get_user_info(tag_user=True) + else: + author_info = "Unknown" - # Print author commit information. - committers_comment += "
        " + if author_info not in committers: + committers[author_info] = [] + + # user commit summary includes the author information and the corresponding commit hash + committers[author_info].append(user_commit_summary) + + # Print the author commit information. github_help_url = "https://help.github.com/en/github/committing-changes-to-your-project/why-are-my-commits-linked-to-the-wrong-user" - for author, commit_hashes in committers.items(): - if author == "Unknown": + for author_info, user_commit_summaries in committers.items(): + if author_info == "Unknown": + # build a quick list of just the commit hash values + commit_shas = [user_commit_summary.commit_sha for user_commit_summary in user_commit_summaries] committers_comment += ( - f"
      • {failed} The commit ({' ,'.join(commit_hashes)}) " - + f"is missing the User's ID, preventing the EasyCLA check. " - + f"Consult GitHub Help to resolve." - + f"For further assistance with EasyCLA, " - + f"please submit a support request ticket." - + "
      • " + f"
      • {failed} The email address for the commit ({', '.join(commit_shas)}) " + "is not linked to the GitHub account, preventing the EasyCLA check. Consult " + f"this Help Article and " + f"GitHub Help to resolve. " + "(To view the commit's email address, add .patch at the end of this PR page's URL.) " + "For further assistance with EasyCLA, " + f"please submit a support request ticket." + "
      • " ) else: - if True in commit_hashes: + missing_affiliations = [ + user_commit_summary + for user_commit_summary in user_commit_summaries + if not user_commit_summary.affiliated and user_commit_summary.authorized + ] + if len(missing_affiliations) > 0: + # build a quick list of just the commit hash values for users missing company affiliations + commit_shas = [ + user_commit_summary.commit_sha + for user_commit_summary in user_commit_summaries + if not user_commit_summary.affiliated + ] + cla.log.info(f"{fn} SHAs for users with missing company affiliations: {commit_shas}") committers_comment += ( - f"
      • {author} ({' ,'.join(commit_hashes[:-1])}) " - + f"is authorized, but they must confirm their affiliation with their company. " - + f"Start the authorization process " - + f" by clicking here, click \"Corporate\"," - + f"select the appropriate company from the list, then confirm " - + f"your affiliation on the page that appears. " - + f"For further assistance with EasyCLA, " - + f"please submit a support request ticket." - + "
      • " + f'
      • {failed} {author_info} ({", ".join(commit_shas)}). ' + f"This user is authorized, but they must confirm their affiliation with their company. " + f"Start the authorization process " + f" by clicking here, click \"Corporate\", " + f"select the appropriate company from the list, then confirm " + f"your affiliation on the page that appears. " + f"For further assistance with EasyCLA, " + f"please submit a support request ticket." + "
      • " ) else: + # build a quick list of just the commit hash values + commit_shas = [user_commit_summary.commit_sha for user_commit_summary in user_commit_summaries] committers_comment += ( - f"
      • " - + f"{failed} - " - + f"{author} The commit ({' ,'.join(commit_hashes)}) is not authorized under a signed CLA. " - + f"Please click here to be authorized. " - + f"For further assistance with EasyCLA, " - + f"please submit a support request ticket." - + "
      • " + f"
      • " + f"{failed} - " + f"{author_info}. The commit ({', '.join(commit_shas)}) " + "is not authorized under a signed CLA. " + f"Please click here to be authorized. " + f"For further assistance with EasyCLA, " + f"please submit a support request ticket." + "
      • " ) + + if len(signed) > 0 or len(missing) > 0: committers_comment += "
      " - return committers_comment - text = "The committers are authorized under a signed CLA." + committers_comment += '' + + if len(signed) > 0 and len(missing) == 0: + text = "The committers listed above are authorized under a signed CLA." + return text + committers_comment @@ -1019,20 +1173,21 @@ def get_authorization_url_and_state(client_id, redirect_uri, scope, authorize_ur :param authorize_url: The URL to submit the OAuth2 request. :type authorize_url: string """ - fn = 'utils.get_authorization_url_and_state' + fn = "utils.get_authorization_url_and_state" oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope) authorization_url, state = oauth.authorization_url(authorize_url) - cla.log.debug(f'{fn} - initialized a new oauth session ' - f'using the github oauth client id: {client_id[0:5]}... ' - f'with the redirect_uri: {redirect_uri} ' - f'using scope of: {scope}. Obtained the ' - f'state: {state} and the ' - f'generated authorization_url: {authorize_url}') + cla.log.debug( + f"{fn} - initialized a new oauth session " + f"using the github oauth client id: {client_id[0:5]}... " + f"with the redirect_uri: {redirect_uri} " + f"using scope of: {scope}. Obtained the " + f"state: {state} and the " + f"generated authorization_url: {authorize_url}" + ) return authorization_url, state -def fetch_token(client_id, state, token_url, client_secret, code, - redirect_uri=None): # pylint: disable=too-many-arguments +def fetch_token(client_id, state, token_url, client_secret, code, redirect_uri=None): # pylint: disable=too-many-arguments """ Helper function to fetch a OAuth2 session token. @@ -1049,16 +1204,18 @@ def fetch_token(client_id, state, token_url, client_secret, code, :param redirect_uri: The redirect URI for this OAuth2 session. :type redirect_uri: string """ - fn = 'utils.fetch_token' + fn = "utils.fetch_token" if redirect_uri is not None: - oauth2 = OAuth2Session(client_id, state=state, scope=['user:email'], redirect_uri=redirect_uri) + oauth2 = OAuth2Session(client_id, state=state, scope=["user:email"], redirect_uri=redirect_uri) else: - oauth2 = OAuth2Session(client_id, state=state, scope=['user:email']) - cla.log.debug(f'{fn} - oauth2.fetch_token - ' - f'token_url: {token_url}, ' - f'client_id: {client_id}, ' - f'client_secret: {client_secret}, ' - f'code: {code}') + oauth2 = OAuth2Session(client_id, state=state, scope=["user:email"]) + cla.log.debug( + f"{fn} - oauth2.fetch_token - " + f"token_url: {token_url}, " + f"client_id: {client_id}, " + f"client_secret: {client_secret}, " + f"code: {code}" + ) return oauth2.fetch_token(token_url, client_secret=client_secret, code=code) @@ -1075,30 +1232,30 @@ def redirect_user_by_signature(user, signature): if signature.get_signature_signed() and signature.get_signature_approved(): # Signature already signed and approved. # TODO: Notify user of signed and approved signature somehow. - cla.log.info('Signature already signed and approved for user: %s, %s', - user.get_user_emails(), signature.get_signature_id()) + cla.log.info( + "Signature already signed and approved for user: %s, %s", user.get_user_emails(), signature.get_signature_id() + ) if return_url is None: - cla.log.info('No return_url set in signature object - serving success message') - return {'status': 'signed and approved'} + cla.log.info("No return_url set in signature object - serving success message") + return {"status": "signed and approved"} else: - cla.log.info('Redirecting user back to %s', return_url) + cla.log.info("Redirecting user back to %s", return_url) raise falcon.HTTPFound(return_url) elif signature.get_signature_signed(): # Awaiting approval. # TODO: Notify user of pending approval somehow. - cla.log.info('Signature signed but not approved yet: %s', - signature.get_signature_id()) + cla.log.info("Signature signed but not approved yet: %s", signature.get_signature_id()) if return_url is None: - cla.log.info('No return_url set in signature object - serving pending message') - return {'status': 'pending approval'} + cla.log.info("No return_url set in signature object - serving pending message") + return {"status": "pending approval"} else: - cla.log.info('Redirecting user back to %s', return_url) + cla.log.info("Redirecting user back to %s", return_url) raise falcon.HTTPFound(return_url) else: # Signature awaiting signature. sign_url = signature.get_signature_sign_url() signature_id = signature.get_signature_id() - cla.log.info('Signature exists, sending user to sign: %s (%s)', signature_id, sign_url) + cla.log.info("Signature exists, sending user to sign: %s (%s)", signature_id, sign_url) raise falcon.HTTPFound(sign_url) @@ -1115,7 +1272,7 @@ def get_active_signature_metadata(user_id): :rtype: dict """ store = get_key_value_store_service() - key = 'active_signature:' + str(user_id) + key = "active_signature:" + str(user_id) if store.exists(key): return json.loads(store.get(key)) return None @@ -1138,13 +1295,12 @@ def set_active_signature_metadata(user_id, project_id, repository_id, pull_reque :type pull_request_id: string """ store = get_key_value_store_service() - key = 'active_signature:' + str(user_id) # Should have been set when user initiated the signature. - value = json.dumps({'user_id': user_id, - 'project_id': project_id, - 'repository_id': repository_id, - 'pull_request_id': pull_request_id}) + key = "active_signature:" + str(user_id) # Should have been set when user initiated the signature. + value = json.dumps( + {"user_id": user_id, "project_id": project_id, "repository_id": repository_id, "pull_request_id": pull_request_id} + ) store.set(key, value) - cla.log.info('Stored active signature details for user %s: Key - %s Value - %s', user_id, key, value) + cla.log.info("Stored active signature details for user %s: Key - %s Value - %s", user_id, key, value) def delete_active_signature_metadata(user_id): @@ -1155,9 +1311,53 @@ def delete_active_signature_metadata(user_id): :type user_id: string """ store = get_key_value_store_service() - key = 'active_signature:' + str(user_id) + key = "active_signature:" + str(user_id) store.delete(key) - cla.log.info('Deleted stored active signature details for user %s', user_id) + cla.log.info("Deleted stored active signature details for user %s", user_id) + + +def set_active_pr_metadata( + github_author_username: str, github_author_email: str, cla_group_id: str, repository_id: str, pull_request_id: str +): + """ + When we receive a GitHub PR callback, we want to store a bit if information/metadata + about the repository, PR, commit authors, and associated CLA Group so that we can later + update the GitHub status check if a CLA manager asynchronously adds one or more commit + authors to the approval list. + This is a helper function to perform the storage of this information. + + :param github_author_username: The GitHub username/logic of the commit author + :type github_author_username: string + :param github_author_email: The GitHub user email of the commit author (if available) + :type github_author_email: string + :param cla_group_id: The ID of the CLA Group + :type cla_group_id: string + :param repository_id: The repository where the PR is coming from. + :type repository_id: str + :param pull_request_id: The PR identifier + :type pull_request_id: str + """ + store = get_key_value_store_service() + + # the same value is stored twice, indexed separately by username and email to allow lookups by either + value = json.dumps( + { + "github_author_username": github_author_username, + "github_author_email": github_author_email, + "cla_group_id": cla_group_id, + "repository_id": repository_id, + "pull_request_id": pull_request_id, + } + ) + + key_github_author_username = "active_pr:u:" + github_author_username + store.set(key_github_author_username, value) + cla.log.info(f"stored active pull request details by user email: %s", key_github_author_username) + + if github_author_email is not None: + key_github_author_email = "active_pr:e:" + github_author_email + store.set(key_github_author_email, value) + cla.log.info(f"stored active pull request details by user email: %s", key_github_author_email) def get_active_signature_return_url(user_id, metadata=None): @@ -1174,29 +1374,30 @@ def get_active_signature_return_url(user_id, metadata=None): if metadata is None: metadata = get_active_signature_metadata(user_id) if metadata is None: - cla.log.warning('Could not find active signature for user {}, return URL request failed'.format(user_id)) + cla.log.warning("Could not find active signature for user {}, return URL request failed".format(user_id)) return None + # Factor in Gitlab flow process + if "merge_request_id" in metadata.keys(): + return metadata["return_url"] + # Get Github ID from metadata - github_repository_id = metadata['repository_id'] + github_repository_id = metadata["repository_id"] # Get installation id through a helper function installation_id = get_installation_id_from_github_repository(github_repository_id) if installation_id is None: - cla.log.error('Could not find installation ID that is configured for this repository ID: %s', - github_repository_id) + cla.log.error("Could not find installation ID that is configured for this repository ID: %s", github_repository_id) return None - github = cla.utils.get_repository_service('github') - return github.get_return_url(metadata['repository_id'], - metadata['pull_request_id'], - installation_id) + github = cla.utils.get_repository_service("github") + return github.get_return_url(metadata["repository_id"], metadata["pull_request_id"], installation_id) def get_installation_id_from_github_repository(github_repository_id): # Get repository ID that references the github ID. try: - repository = Repository().get_repository_by_external_id(github_repository_id, 'github') + repository = Repository().get_repository_by_external_id(github_repository_id, "github") except DoesNotExist: return None @@ -1211,10 +1412,28 @@ def get_installation_id_from_github_repository(github_repository_id): return organization.get_organization_installation_id() +def get_organization_id_from_gitlab_repository(gitlab_repository_id): + # Get repository ID that references the gitlab ID. + try: + repository = Repository().get_repository_by_external_id(gitlab_repository_id, "gitlab") + except DoesNotExist: + return None + # Get GitLabGroup from this repository + gitLabOrg = None + try: + gitLabOrg = GitlabOrg().search_organization_by_lower_name(repository.get_repository_organization_name().lower()) + except DoesNotExist: + cla.log.debug(f"unable to get gitlab org by name: {repository.get_repository_organization_name()}") + return None + + # return GitLab organization ID + return gitLabOrg.get_organization_id() + + def get_project_id_from_github_repository(github_repository_id): # Get repository ID that references the github ID. try: - repository = Repository().get_repository_by_external_id(github_repository_id, 'github') + repository = Repository().get_repository_by_external_id(github_repository_id, "github") except DoesNotExist: return None @@ -1236,21 +1455,64 @@ def get_individual_signature_callback_url(user_id, metadata=None): if metadata is None: metadata = get_active_signature_metadata(user_id) if metadata is None: - cla.log.warning('Could not find active signature for user {}, callback URL request failed'.format(user_id)) + cla.log.warning("Could not find active signature for user {}, callback URL request failed".format(user_id)) return None # Get Github ID from metadata - github_repository_id = metadata['repository_id'] + github_repository_id = metadata["repository_id"] # Get installation id through a helper function installation_id = get_installation_id_from_github_repository(github_repository_id) if installation_id is None: - cla.log.error('Could not find installation ID that is configured for this repository ID: %s', - github_repository_id) + cla.log.error("Could not find installation ID that is configured for this repository ID: %s", github_repository_id) return None - return os.path.join(API_BASE_URL, 'v2/signed/individual', str(installation_id), str(metadata['repository_id']), - str(metadata['pull_request_id'])) + return os.path.join( + API_BASE_URL, + "v2/signed/individual", + str(installation_id), + str(metadata["repository_id"]), + str(metadata["pull_request_id"]), + ) + + +def get_individual_signature_callback_url_gitlab(user_id, metadata=None): + """ + Helper function to get a user's active signature callback URL. + + :param user_id: The user ID in question. + :type user_id: string + :param metadata: The signature metadata + :type metadata: dict + :return: The callback URL that will be hit by the signing service provider. + :rtype: string + """ + if metadata is None: + metadata = get_active_signature_metadata(user_id) + if metadata is None: + cla.log.warning("Could not find active signature for user {}, callback URL request failed".format(user_id)) + return None + + # Get GitLab ID from metadata + gitlab_repository_id = metadata["repository_id"] + + # Get organization id + organization_id = get_organization_id_from_gitlab_repository(gitlab_repository_id) + + if organization_id is None: + cla.log.error( + "Could not find GitLab organization ID that is configured for this repository ID: %s", gitlab_repository_id + ) + return None + + return os.path.join( + API_BASE_URL, + "v2/signed/gitlab/individual", + str(user_id), + str(organization_id), + str(metadata["repository_id"]), + str(metadata["merge_request_id"]), + ) def request_individual_signature(installation_id, github_repository_id, user, change_request_id, callback_url=None): @@ -1272,27 +1534,66 @@ def request_individual_signature(installation_id, github_repository_id, user, ch :type callback_url: string """ project_id = get_project_id_from_github_repository(github_repository_id) - repo_service = get_repository_service('github') - return_url = repo_service.get_return_url(github_repository_id, - change_request_id, - installation_id) + repo_service = get_repository_service("github") + return_url = repo_service.get_return_url(github_repository_id, change_request_id, installation_id) if callback_url is None: - callback_url = os.path.join(API_BASE_URL, 'v2/signed/individual', str(installation_id), str(change_request_id)) + callback_url = os.path.join(API_BASE_URL, "v2/signed/individual", str(installation_id), str(change_request_id)) signing_service = get_signing_service() - return_url_type = 'Github' - signature_data = signing_service.request_individual_signature(project_id, - user.get_user_id(), - return_url_type, - return_url, - callback_url) - if 'sign_url' in signature_data: - raise falcon.HTTPFound(signature_data['sign_url']) - cla.log.error('Could not get sign_url from signing service provider - sending user ' - 'to return_url instead') + return_url_type = "Github" + signature_data = signing_service.request_individual_signature( + project_id, user.get_user_id(), return_url_type, return_url, callback_url + ) + if "sign_url" in signature_data: + raise falcon.HTTPFound(signature_data["sign_url"]) + cla.log.error("Could not get sign_url from signing service provider - sending user " "to return_url instead") raise falcon.HTTPFound(return_url) +def lookup_user_gitlab_username(user_gitlab_id: int) -> Optional[str]: + """ + Given a user gitlab ID, looks up the user's gitlab login/username. + :param user_gitlab_id: the gitlab id + :return: the user's gitlab login/username + """ + try: + r = requests.get(f"https://gitlab.com/api/v4/users/{user_gitlab_id}") + r.raise_for_status() + except requests.exceptions.HTTPError as err: + msg = f"Could not get user github user from id: {user_gitlab_id}: error: {err}" + cla.log.warning(msg) + return None + + gitlab_user = r.json() + if "id" in gitlab_user: + return gitlab_user["id"] + else: + cla.log.warning('Malformed HTTP response from GitLab - expecting "id" attribute ' f"- response: {gitlab_user}") + return None + + +def lookup_user_gitlab_id(user_gitlab_username: str) -> Optional[str]: + """ + Given a user gitlab username, looks up the user's gitlab id. + :param user_gitlab_username: the gitlab username + :return: the user's gitlab id + """ + try: + r = requests.get(f"https://gitlab.com/api/v4/users?username={user_gitlab_username}") + r.raise_for_status() + except requests.exceptions.HTTPError as err: + msg = f"Could not get user github user from username: {user_gitlab_username}: error: {err}" + cla.log.warning(msg) + return None + + gitlab_user = r.json() + if "username" in gitlab_user: + return gitlab_user["username"] + else: + cla.log.warning('Malformed HTTP response from GitLab - expecting "username" attribute ' f"- response: {gitlab_user}") + return None + + def lookup_user_github_username(user_github_id: int) -> Optional[str]: """ Given a user github ID, looks up the user's github login/username. @@ -1301,28 +1602,28 @@ def lookup_user_github_username(user_github_id: int) -> Optional[str]: """ try: headers = { - 'Authorization': 'Bearer {}'.format(cla.conf['GITHUB_OAUTH_TOKEN']), - 'Accept': 'application/json', + "Authorization": "Bearer {}".format(cla.conf["GITHUB_OAUTH_TOKEN"]), + "Accept": "application/json", } - r = requests.get(f'https://api.github.com/user/{user_github_id}', headers=headers) + r = requests.get(f"https://api.github.com/user/{user_github_id}", headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as err: - msg = f'Could not get user github user from id: {user_github_id}: error: {err}' + msg = f"Could not get user github user from id: {user_github_id}: error: {err}" cla.log.warning(msg) return None github_user = r.json() - if 'message' in github_user: - cla.log.warning(f'Unable to lookup user from id: {user_github_id} ' - f'- message: {github_user["message"]}') + if "message" in github_user: + cla.log.warning(f"Unable to lookup user from id: {user_github_id} " f'- message: {github_user["message"]}') return None else: - if 'login' in github_user: - return github_user['login'] + if "login" in github_user: + return github_user["login"] else: - cla.log.warning('Malformed HTTP response from GitHub - expecting "login" attribute ' - f'- response: {github_user}') + cla.log.warning( + 'Malformed HTTP response from GitHub - expecting "login" attribute ' f"- response: {github_user}" + ) return None @@ -1334,28 +1635,26 @@ def lookup_user_github_id(user_github_username: str) -> Optional[int]: """ try: headers = { - 'Authorization': 'Bearer {}'.format(cla.conf['GITHUB_OAUTH_TOKEN']), - 'Accept': 'application/json', + "Authorization": "Bearer {}".format(cla.conf["GITHUB_OAUTH_TOKEN"]), + "Accept": "application/json", } - r = requests.get(f'https://api.github.com/users/{user_github_username}', headers=headers) + r = requests.get(f"https://api.github.com/users/{user_github_username}", headers=headers) r.raise_for_status() except requests.exceptions.HTTPError as err: - msg = f'Could not get user github id from username: {user_github_username}: error: {err}' + msg = f"Could not get user github id from username: {user_github_username}: error: {err}" cla.log.warning(msg) return None github_user = r.json() - if 'message' in github_user: - cla.log.warning(f'Unable to lookup user from id: {user_github_username} ' - f'- message: {github_user["message"]}') + if "message" in github_user: + cla.log.warning(f"Unable to lookup user from id: {user_github_username} " f'- message: {github_user["message"]}') return None else: - if 'id' in github_user: - return github_user['id'] + if "id" in github_user: + return github_user["id"] else: - cla.log.warning('Malformed HTTP response from GitHub - expecting "id" attribute ' - f'- response: {github_user}') + cla.log.warning('Malformed HTTP response from GitHub - expecting "id" attribute ' f"- response: {github_user}") return None @@ -1363,16 +1662,27 @@ def lookup_github_organizations(github_username: str): # Use the Github API to retrieve github orgs that the user is a member of (user must be a public member). try: headers = { - 'Authorization': 'Bearer {}'.format(cla.conf['GITHUB_OAUTH_TOKEN']), - 'Accept': 'application/json', + "Authorization": "Bearer {}".format(cla.conf["GITHUB_OAUTH_TOKEN"]), + "Accept": "application/json", } - r = requests.get(f'https://api.github.com/users/{github_username}/orgs', headers=headers) + r = requests.get(f"https://api.github.com/users/{github_username}/orgs", headers=headers) + r.raise_for_status() + except requests.exceptions.HTTPError as err: + cla.log.warning("Could not get user github org: {}".format(err)) + return {"error": "Could not get user github org: {}".format(err)} + return [github_org["login"] for github_org in r.json()] + + +def lookup_gitlab_org_members(organization_id): + # Use the v2 Endpoint thats a wrapper for Gitlab Group member query + try: + r = requests.get(f"{cla.config.PLATFORM_GATEWAY_URL}/cla-service/v4/gitlab/group/{organization_id}/members") r.raise_for_status() except requests.exceptions.HTTPError as err: - cla.log.warning('Could not get user github org: {}'.format(err)) - return {'error': 'Could not get user github org: {}'.format(err)} - return [github_org['login'] for github_org in r.json()] + cla.log.warning(f"Could not fetch gitlab org users: {err}") + return {f"error: Could not get user gitlab group id: {organization_id} members: {err}"} + return r.json()["list"] def update_github_username(github_user: dict, user: User): @@ -1384,16 +1694,18 @@ def update_github_username(github_user: dict, user: User): :return: None """ # set the github username if available - if 'login' in github_user: + if "login" in github_user: if user.get_user_github_username() is None: cla.log.debug(f'Updating user record - adding github username: {github_user["login"]}') - user.set_user_github_username(github_user['login']) - if user.get_user_github_username() != github_user['login']: - cla.log.warning(f'Note: github user with id: {github_user["id"]}' - f' has a mismatched username (gh: {github_user["id"]} ' - f'vs db user record: {user.get_user_github_username}) - ' - f'setting the value to: {github_user["login"]}') - user.set_user_github_username(github_user['login']) + user.set_user_github_username(github_user["login"]) + if user.get_user_github_username() != github_user["login"]: + cla.log.warning( + f'Note: github user with id: {github_user["id"]}' + f' has a mismatched username (gh: {github_user["id"]} ' + f"vs db user record: {user.get_user_github_username}) - " + f'setting the value to: {github_user["login"]}' + ) + user.set_user_github_username(github_user["login"]) def is_approved(ccla_signature: Signature, email=None, github_username=None, github_id=None): @@ -1407,28 +1719,29 @@ def is_approved(ccla_signature: Signature, email=None, github_username=None, git :param github_username: A given github username checked against ccla signature github/github-org whitelists :param github_id: A given github id checked against ccla signature github/github-org whitelists """ - fn = 'utils.is_approved' + fn = "utils.is_approved" if email: # Checking email whitelist whitelist = ccla_signature.get_email_whitelist() - cla.log.debug(f'{fn} - testing email: {email} with CCLA approval list emails: {whitelist}') + cla.log.debug(f"{fn} - testing email: {email} with CCLA approval list emails: {whitelist}") if whitelist is not None: if email.lower() in (s.lower() for s in whitelist): - cla.log.debug(f'{fn} found user email in email approval list') + cla.log.debug(f"{fn} found user email in email approval list") return True # Checking domain whitelist patterns = ccla_signature.get_domain_whitelist() - cla.log.debug(f"{fn} - testing user email domain: {email} with " - f"domain approval list values in database: {patterns}") + cla.log.debug( + f"{fn} - testing user email domain: {email} with " f"domain approval list values in database: {patterns}" + ) if patterns is not None: if get_user_instance().preprocess_pattern([email], patterns): return True else: cla.log.debug(f"{fn} - did not match email: {email} with domain: {patterns}") else: - cla.log.debug(f'{fn} - no domain approval list patterns defined - skipping domain approval list check') + cla.log.debug(f"{fn} - no domain approval list patterns defined - skipping domain approval list check") if github_id: github_username = lookup_user_github_username(github_id) @@ -1438,16 +1751,18 @@ def is_approved(ccla_signature: Signature, email=None, github_username=None, git # remove leading and trailing whitespace from github username github_username = github_username.strip() github_approval_list = ccla_signature.get_github_whitelist() - cla.log.debug(f"{fn} - testing user github username: {github_username} with " - f"CCLA github approval list: {github_approval_list}") + cla.log.debug( + f"{fn} - testing user github username: {github_username} with " + f"CCLA github approval list: {github_approval_list}" + ) if github_approval_list is not None: # case insensitive search if github_username.lower() in (s.lower() for s in github_approval_list): - cla.log.debug(f'{fn} - found github username in github approval list') + cla.log.debug(f"{fn} - found github username in github approval list") return True else: - cla.log.debug(f'{fn} - users github_username is not defined - skipping github username approval list check') + cla.log.debug(f"{fn} - users github_username is not defined - skipping github username approval list check") # Check github org approval list if github_username is not None: @@ -1455,29 +1770,31 @@ def is_approved(ccla_signature: Signature, email=None, github_username=None, git if "error" not in github_orgs: # Fetch the list of orgs this user is part of github_org_approval_list = ccla_signature.get_github_org_whitelist() - cla.log.debug(f'{fn} - testing user github orgs: {github_orgs} with ' - f'CCLA github org approval list values: {github_org_approval_list}') + cla.log.debug( + f"{fn} - testing user github orgs: {github_orgs} with " + f"CCLA github org approval list values: {github_org_approval_list}" + ) if github_org_approval_list is not None: for dynamo_github_org in github_org_approval_list: # case insensitive search if dynamo_github_org.lower() in (s.lower() for s in github_orgs): - cla.log.debug(f'{fn} - found matching github org for user') + cla.log.debug(f"{fn} - found matching github org for user") return True else: - cla.log.debug(f'{fn} - users github_username is not defined - skipping github org approval list check') + cla.log.debug(f"{fn} - users github_username is not defined - skipping github org approval list check") - cla.log.debug(f'{fn} - unable to find user in any approval list') + cla.log.debug(f"{fn} - unable to find user in any approval list") return False def audit_event(func): - """ Decorator that audits events """ + """Decorator that audits events""" def wrapper(**kwargs): response = func(**kwargs) if response.get("status_code") == falcon.HTTP_200: - cla.log.debug("Created event {} ".format(kwargs['event_type'])) + cla.log.debug("Created event {} ".format(kwargs["event_type"])) else: cla.log.debug("Failed to add event") return response @@ -1486,7 +1803,7 @@ def wrapper(**kwargs): def get_oauth_client(): - return OAuth2Session(os.environ['GH_OAUTH_CLIENT_ID']) + return OAuth2Session(os.environ["GH_OAUTH_CLIENT_ID"]) def fmt_project(project: Project): @@ -1494,39 +1811,42 @@ def fmt_project(project: Project): def fmt_company(company: Company): - return "{} ({}) - acl: {}".format( - company.get_company_name(), - company.get_company_id(), - company.get_company_acl()) + return "{} ({}) - acl: {}".format(company.get_company_name(), company.get_company_id(), company.get_company_acl()) def fmt_user(user: User): - return '{} ({}) {}'.format( - user.get_user_name(), - user.get_user_id(), - user.get_lf_email()) + return "{} ({}) {}".format(user.get_user_name(), user.get_user_id(), user.get_lf_email()) def fmt_users(users: List[User]): - response = '' + response = "" for user in users: - response += fmt_user(user) + ' ' + response += fmt_user(user) + " " return response def get_email_help_content(show_v2_help_link: bool) -> str: # v1 help link - help_link = 'https://docs.linuxfoundation.org/lfx/easycla' + help_link = "https://docs.linuxfoundation.org/lfx/easycla" if show_v2_help_link: # v2 help link - help_link = 'https://docs.linuxfoundation.org/lfx/v/v2/easycla' + help_link = "https://docs.linuxfoundation.org/lfx/easycla" return f'

      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--github-orgs the table" runtime: go1.x package: individually: true - include: - - ./dynamo-events-lambda + patterns: + - '!**' + - 'bin/dynamo-events-lambda' - saveMetrics: + save-metrics-lambda: + name: ${self:service}-${sls:stage, 'dev'}-save-metrics-lambda description: "EasyCLA Save Metrics API handler" runtime: go1.x - handler: metrics-aws-lambda + handler: 'bin/metrics-aws-lambda' timeout: 900 # maximum time allowed events: - schedule: @@ -474,13 +495,15 @@ functions: enabled: true package: individually: true - include: - - ./metrics-aws-lambda + patterns: + - '!**' + - 'bin/metrics-aws-lambda' - reportMetrics: + report-metrics-lambda: + name: ${self:service}-${sls:stage, 'dev'}-report-metrics-lambda description: "EasyCLA Report Metrics API handler" runtime: go1.x - handler: metrics-report-lambda + handler: 'bin/metrics-report-lambda' timeout: 900 # maximum time allowed events: - schedule: @@ -489,14 +512,14 @@ functions: enabled: true package: individually: true - include: - - ./metrics-report-lambda - + patterns: + - '!**' + - 'bin/metrics-report-lambda' - zipbuilder-scheduler-lambda: - handler: zipbuilder-scheduler-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-zipbuilder-scheduler-lambda + zip-builder-scheduler-lambda: + name: ${self:service}-${sls:stage, 'dev'}-zip-builder-scheduler-lambda description: "call zipbuilder-lambda for all cla groups periodically" + handler: 'bin/zipbuilder-scheduler-lambda' runtime: go1.x timeout: 900 # maximum time allowed events: @@ -506,20 +529,56 @@ functions: enabled: true package: individually: true - include: - - ./zipbuilder-scheduler-lambda + patterns: + - '!**' + - 'bin/zipbuilder-scheduler-lambda' - zipbuilder-lambda: - handler: zipbuilder-lambda - name: ${self:service}-${opt:stage, self:provider.stage, 'dev'}-zipbuilder-lambda + zip-builder-lambda: + handler: 'bin/zipbuilder-lambda' + name: ${self:service}-${sls:stage, 'dev'}-zip-builder-lambda description: "build zip of signed signature pdf for cla group" runtime: go1.x timeout: 900 # maximum time allowed memorySize: 1024 package: individually: true - include: - - ./zipbuilder-lambda + patterns: + - '!**' + - 'bin/zipbuilder-lambda' + + gitlab-repository-check-lambda: + handler: 'bin/gitlab-repository-check-lambda' + name: ${self:service}-${sls:stage, 'dev'}-gitlab-repository-check-lambda + description: "routine to periodically check the GitLab repository list for auto-enabled GitLab Groups" + runtime: go1.x + timeout: 900 # maximum time allowed + memorySize: 1024 + events: + - schedule: + description: 'periodically check the GitLab repository list for auto-enabled GitLab Groups' + rate: rate(15 minutes) + enabled: true + package: + individually: true + patterns: + - '!**' + - 'bin/gitlab-repository-check-lambda' + + # User Subscribe event for dynamodb cla-stage-users table. + easycla-user-event-handler-lambda: + handler: 'bin/user-subscribe-lambda' + name: ${self:service}-${sls:stage, 'dev'}-user-event-handler-lambda + runtime: go1.x + description: Update easycla user data to user object in dynamodb + package: + individually: true + patterns: + - '!**' + - 'bin/user-subscribe-lambda' + reservedConcurrency: 5 + events: + - sns: + arn: ${self:custom.userEventsSNSTopicARN} apiv1: handler: wsgi_handler.handler @@ -575,20 +634,6 @@ functions: method: POST path: v2/github/activity - # User Subscribe event for dynamodb cla-stage-users table. - easyClaUserSubscribe: - name: easy-cla-user-subscribe - runtime: go1.x - description: Update easycla user data to user object in dynamodb - handler: user-subscribe-lambda - package: - individually: true - include: - - ./user-subscribe-lambda - reservedConcurrency: 5 - events: - - sns: - arn: ${self:custom.userEventsSNSTopicARN} resources: Conditions: @@ -597,7 +642,7 @@ resources: isProd: { "Fn::Equals": [ "${env:STAGE}", "prod" ] } isStaging: { "Fn::Equals": [ "${env:STAGE}", "staging" ] } isDev: { "Fn::Equals": [ "${env:STAGE}", "dev" ] } - isNotProd: {"Fn::Or": [{"Condition": "isDev"}, {"Condition": "isStaging" }]} + isNotProd: { "Fn::Or": [ { "Condition": "isDev" }, { "Condition": "isStaging" } ] } # true when a TSL certificate should be created by serverless (false created externally) ShouldGenerateCertificate: Fn::Not: [ Fn::Equals: [ "${env:STAGE}", "prod" ] ] @@ -610,7 +655,7 @@ resources: ApiGatewayRestApi: Type: AWS::ApiGateway::RestApi Properties: - Name: ${self:service}-${self:provider.stage} + Name: ${self:service}-${sls:stage} Description: EasyCLA API Gateway GatewayResponse: @@ -627,9 +672,9 @@ resources: Type: AWS::CertificateManager::Certificate Condition: ShouldGenerateCertificate Properties: - DomainName: ${self:custom.product.domain.name.${opt:stage}, self:custom.product.domain.name.other} + DomainName: ${self:custom.product.domain.name.${sls:stage}, self:custom.product.domain.name.other} SubjectAlternativeNames: - - ${self:custom.product.domain.alt.${opt:stage}, self:custom.product.domain.alt.other} + - ${self:custom.product.domain.alt.${sls:stage}, self:custom.product.domain.alt.other} ValidationMethod: DNS Outputs: diff --git a/cla-backend/yarn.lock b/cla-backend/yarn.lock index 1ba44bbbb..5331fc1f3 100644 --- a/cla-backend/yarn.lock +++ b/cla-backend/yarn.lock @@ -4,331 +4,1120 @@ "2-thenable@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/2-thenable/-/2-thenable-1.0.0.tgz#56e9a2e363293b1e507f501aac1aa9927670b2fc" + resolved "https://registry.npmjs.org/2-thenable/-/2-thenable-1.0.0.tgz" integrity sha512-HqiDzaLDFCXkcCO/SwoyhRwqYtINFHF7t9BDRq4x90TOKNAJpiqUt9X5lQ08bwxYzc067HUywDjGySpebHcUpw== dependencies: d "1" es5-ext "^0.10.47" -"@babel/runtime@^7.3.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" - 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" - integrity sha512-t6pTJdsV0qhh9YX22/Npsv95GqVABc5GRInSK7JSSNIpPLq9TM+K7odYzcOuQRPZAD9OHxZfbYsB4WJOalzqng== - dependencies: - es6-promisify "^6.0.0" - pipe-io "^3.0.0" - wraptile "^2.0.0" - zames "^2.0.0" - -"@hapi/accept@^3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-3.2.4.tgz#687510529493fe1d7d47954c31aff360d9364bd1" - integrity sha512-soThGB+QMgfxlh0Vzhzlf3ZOEOPk5biEwcOXhkF0Eedqx8VnhGiggL9UYHrIsOb1rUg3Be3K8kp0iDL2wbVSOQ== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "8.x.x" - -"@hapi/address@2.x.x", "@hapi/address@^2.1.2": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" - integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== - -"@hapi/ammo@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-3.1.2.tgz#a9edf5d48d99b75fdcd7ab3dabf9059942a06961" - integrity sha512-ej9OtFmiZv1qr45g1bxEZNGyaR4jRpyMxU6VhbxjaYThymvOwsyIsUKMZnP5Qw2tfYFuwqCJuIBHGpeIbdX9gQ== - dependencies: - "@hapi/hoek" "8.x.x" - -"@hapi/b64@4.x.x": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-4.2.1.tgz#bf8418d7907c5e73463f2e3b5c6fca7e9f2a1357" - integrity sha512-zqHpQuH5CBMw6hADzKfU/IGNrxq1Q+/wTYV+OiZRQN9F3tMyk+9BUMeBvFRMamduuqL8iSp62QAnJ+7ATiYLWA== - dependencies: - "@hapi/hoek" "8.x.x" - -"@hapi/boom@7.x.x", "@hapi/boom@^7.4.11": - version "7.4.11" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-7.4.11.tgz#37af8417eb9416aef3367aa60fa04a1a9f1fc262" - integrity sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A== - dependencies: - "@hapi/hoek" "8.x.x" - -"@hapi/bounce@1.x.x": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-1.3.2.tgz#3b096bb02f67de6115e6e4f0debc390be5a86bad" - integrity sha512-3bnb1AlcEByFZnpDIidxQyw1Gds81z/1rSqlx4bIEE+wUN0ATj0D49B5cE1wGocy90Rp/de4tv7GjsKd5RQeew== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "^8.3.1" - -"@hapi/bourne@1.x.x": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" - integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== - -"@hapi/call@^5.1.3": - version "5.1.3" - resolved "https://registry.yarnpkg.com/@hapi/call/-/call-5.1.3.tgz#217af45e3bc3d38b03aa5c9edfe1be939eee3741" - integrity sha512-5DfWpMk7qZiYhvBhM5oUiT4GQ/O8a2rFR121/PdwA/eZ2C1EsuD547ZggMKAR5bZ+FtxOf0fdM20zzcXzq2mZA== +"@aws-crypto/crc32@3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz" + integrity sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA== dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "8.x.x" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" -"@hapi/catbox-memory@4.x.x": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-4.1.1.tgz#263a6f3361f7a200552c5772c98a8e80a1da712f" - integrity sha512-T6Hdy8DExzG0jY7C8yYWZB4XHfc0v+p1EGkwxl2HoaPYAmW7I3E59M/IvmSVpis8RPcIoBp41ZpO2aZPBpM2Ww== +"@aws-crypto/crc32c@3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz" + integrity sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w== dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "8.x.x" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" -"@hapi/catbox@10.x.x": - version "10.2.3" - resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-10.2.3.tgz#2df51ab943d7613df3718fa2bfd981dd9558cec5" - integrity sha512-kN9hXO4NYyOHW09CXiuj5qW1syc/0XeVOBsNNk0Tz89wWNQE5h21WF+VsfAw3uFR8swn/Wj3YEVBnWqo82m/JQ== +"@aws-crypto/ie11-detection@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz" + integrity sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q== dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/joi" "16.x.x" - "@hapi/podium" "3.x.x" + tslib "^1.11.1" -"@hapi/content@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@hapi/content/-/content-4.1.1.tgz#179673d1e2b7eb36c564d8f9605d019bd2252cbf" - integrity sha512-3TWvmwpVPxFSF3KBjKZ8yDqIKKZZIm7VurDSweYpXYENZrJH3C1hd1+qEQW9wQaUaI76pPBLGrXl6I3B7i3ipA== +"@aws-crypto/sha1-browser@3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz" + 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.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz" + 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.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz" + integrity sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ== dependencies: - "@hapi/boom" "7.x.x" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" -"@hapi/cryptiles@4.x.x": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-4.2.1.tgz#ff0f18d79074659838caedbb911851313ad1ffbc" - integrity sha512-XoqgKsHK0l/VpqPs+tr6j6vE+VQ3+2bkF2stvttmc7xAOf1oSAwHcJ0tlp/6MxMysktt1IEY0Csy3khKaP9/uQ== +"@aws-crypto/supports-web-crypto@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz" + integrity sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg== dependencies: - "@hapi/boom" "7.x.x" + tslib "^1.11.1" -"@hapi/file@1.x.x": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@hapi/file/-/file-1.0.0.tgz#c91c39fd04db8bed5af82d2e032e7a4e65555b38" - integrity sha512-Bsfp/+1Gyf70eGtnIgmScvrH8sSypO3TcK3Zf0QdHnzn/ACnAkI6KLtGACmNRPEzzIy+W7aJX5E+1fc9GwIABQ== +"@aws-crypto/util@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz" + 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/abort-controller@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.357.0.tgz" + integrity sha512-nQYDJon87quPwt2JZJwUN2GFKJnvE5kWb6tZP4xb5biSGUKBqDQo06oYed7yokatCuCMouIXV462aN0fWODtOw== + dependencies: + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/chunked-blob-reader@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.310.0.tgz" + integrity sha512-CrJS3exo4mWaLnWxfCH+w88Ou0IcAZSIkk4QbmxiHl/5Dq705OLoxf4385MVyExpqpeVJYOYQ2WaD8i/pQZ2fg== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/client-acm@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-acm/-/client-acm-3.363.0.tgz" + integrity sha512-NFwpEOu+3EUnHxiPZV1ci142l2b5qNCYZBkJiuV2Wf7mLMbMESfaO2/0QNdZQEBbCwZDou1/VBdchv52D4nzJg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-utf8" "^1.0.1" + "@smithy/util-waiter" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/client-api-gateway@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-api-gateway/-/client-api-gateway-3.363.0.tgz" + integrity sha512-FAAbPhfsluLbD/uWfbdd6tcJsYd//N7ImSeGuuT3m18mWNmLTs3skX0sAv5KGDk9dCZiVK5G7juvYoLWYvl5rQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-sdk-api-gateway" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-stream" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/client-apigatewayv2@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-apigatewayv2/-/client-apigatewayv2-3.363.0.tgz" + integrity sha512-nHe8p4ZRlrUaMCFbNW+8q9cRb5nw5kl1GitEw9Dmc0lRyie6rY4b6LD9MGQ2ZBFNp7MlccJoBoOc2hTo7VkP0A== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-stream" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/client-cloudformation@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.363.0.tgz" + integrity sha512-yoPKtRVLsnNfmm8QOnfgA+fgXJsqCatQnuwQNAPQfxpTZ4ZEZoyEO+EtVXpYdibS/Hifn6w84BHN/S4VS8moJg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-utf8" "^1.0.1" + "@smithy/util-waiter" "^1.0.1" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + uuid "^8.3.2" -"@hapi/formula@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd" - integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA== +"@aws-sdk/client-cognito-identity@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.363.0.tgz" + integrity sha512-tsJzgBSCpna85IVsuS7FBIK9wkSl7fs8TJ/QzapIgu8rKss0ySHVO6TeMVAdw2BvaQl7CxU9c3PosjhLWHu6KQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/client-route-53@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-route-53/-/client-route-53-3.363.0.tgz" + integrity sha512-SLBgIVjpu0jr5nKMv5syCyBcC0vPJaibAfGXhBtmWdWN3L5XegKSY8O4axgjgHqqwsKbnryZj9/6D4kn6ATWTQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-sdk-route53" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@aws-sdk/xml-builder" "3.310.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-utf8" "^1.0.1" + "@smithy/util-waiter" "^1.0.1" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + +"@aws-sdk/client-s3@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.363.0.tgz" + integrity sha512-LNnfg/t8wG5Fqj6l+PSV/t+IXDq9r3Kj9jEHn84513+p7bewXYSSreSpmLjG8OcKuMfHc9EJGNQ3DkMyFaLoWg== + 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-sts" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/hash-blob-browser" "3.357.0" + "@aws-sdk/hash-stream-node" "3.357.0" + "@aws-sdk/md5-js" "3.357.0" + "@aws-sdk/middleware-bucket-endpoint" "3.363.0" + "@aws-sdk/middleware-expect-continue" "3.363.0" + "@aws-sdk/middleware-flexible-checksums" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-location-constraint" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-sdk-s3" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-ssec" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/signature-v4-multi-region" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@aws-sdk/xml-builder" "3.310.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/eventstream-serde-browser" "^1.0.1" + "@smithy/eventstream-serde-config-resolver" "^1.0.1" + "@smithy/eventstream-serde-node" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-stream" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + "@smithy/util-waiter" "^1.0.1" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + +"@aws-sdk/client-sso-oidc@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.363.0.tgz" + integrity sha512-V3Ebiq/zNtDS/O92HUWGBa7MY59RYSsqWd+E0XrXv6VYTA00RlMTbNcseivNgp2UghOgB9a20Nkz6EqAeIN+RQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/client-sso@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.363.0.tgz" + integrity sha512-PZ+HfKSgS4hlMnJzG+Ev8/mgHd/b/ETlJWPSWjC/f2NwVoBQkBnqHjdyEx7QjF6nksJozcVh5Q+kkYLKc/QwBQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.2" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/protocol-http" "^1.0.1" + "@smithy/smithy-client" "^1.0.3" + "@smithy/types" "^1.0.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.2" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/client-sts@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.363.0.tgz" + integrity sha512-0jj14WvBPJQ8xr72cL0mhlmQ90tF0O0wqXwSbtog6PsC8+KDE6Yf+WsxsumyI8E5O8u3eYijBL+KdqG07F/y/w== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/middleware-host-header" "3.363.0" + "@aws-sdk/middleware-logger" "3.363.0" + "@aws-sdk/middleware-recursion-detection" "3.363.0" + "@aws-sdk/middleware-sdk-sts" "3.363.0" + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/middleware-user-agent" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@aws-sdk/util-user-agent-browser" "3.363.0" + "@aws-sdk/util-user-agent-node" "3.363.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/hash-node" "^1.0.1" + "@smithy/invalid-dependency" "^1.0.1" + "@smithy/middleware-content-length" "^1.0.1" + "@smithy/middleware-endpoint" "^1.0.1" + "@smithy/middleware-retry" "^1.0.1" + "@smithy/middleware-serde" "^1.0.1" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/node-http-handler" "^1.0.1" + "@smithy/protocol-http" "^1.1.0" + "@smithy/smithy-client" "^1.0.2" + "@smithy/types" "^1.1.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-body-length-browser" "^1.0.1" + "@smithy/util-body-length-node" "^1.0.1" + "@smithy/util-defaults-mode-browser" "^1.0.1" + "@smithy/util-defaults-mode-node" "^1.0.1" + "@smithy/util-retry" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + +"@aws-sdk/config-resolver@^3.329.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.357.0.tgz" + integrity sha512-cukfg0nX7Tzx/xFyH5F4Eyb8DA1ITCGtSQv4vnEjgUop+bkzckuGLKEeBcBhyZY+aw+2C9CVwIHwIMhRm0ul5w== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-config-provider" "3.310.0" + "@aws-sdk/util-middleware" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-cognito-identity@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.363.0.tgz" + integrity sha512-5x42JvqEsBUrm6/qdf0WWe4mlmJjPItxamQhRjuOzeQD/BxsA2W5VS/7n0Ws0e27DNhlnUErcIJd+bBy6j1fqA== + dependencies: + "@aws-sdk/client-cognito-identity" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-env@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.363.0.tgz" + integrity sha512-VAQ3zITT2Q0acht0HezouYnMFKZ2vIOa20X4zQA3WI0HfaP4D6ga6KaenbDcb/4VFiqfqiRHfdyXHP0ThcDRMA== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-ini@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.363.0.tgz" + integrity sha512-ZYN+INoqyX5FVC3rqUxB6O8nOWkr0gHRRBm1suoOlmuFJ/WSlW/uUGthRBY5x1AQQnBF8cpdlxZzGHd41lFVNw== + dependencies: + "@aws-sdk/credential-provider-env" "3.363.0" + "@aws-sdk/credential-provider-process" "3.363.0" + "@aws-sdk/credential-provider-sso" "3.363.0" + "@aws-sdk/credential-provider-web-identity" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/credential-provider-imds" "^1.0.1" + "@smithy/property-provider" "^1.0.1" + "@smithy/shared-ini-file-loader" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-node@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.363.0.tgz" + integrity sha512-C1qXFIN2yMxD6pGgug0vR1UhScOki6VqdzuBHzXZAGu7MOjvgHNdscEcb3CpWnITHaPL2ztkiw75T1sZ7oIgQg== + dependencies: + "@aws-sdk/credential-provider-env" "3.363.0" + "@aws-sdk/credential-provider-ini" "3.363.0" + "@aws-sdk/credential-provider-process" "3.363.0" + "@aws-sdk/credential-provider-sso" "3.363.0" + "@aws-sdk/credential-provider-web-identity" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/credential-provider-imds" "^1.0.1" + "@smithy/property-provider" "^1.0.1" + "@smithy/shared-ini-file-loader" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-process@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.363.0.tgz" + integrity sha512-fOKAINU7Rtj2T8pP13GdCt+u0Ml3gYynp8ki+1jMZIQ+Ju/MdDOqZpKMFKicMn3Z1ttUOgqr+grUdus6z8ceBQ== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/shared-ini-file-loader" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-sso@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.363.0.tgz" + integrity sha512-5RUZ5oM0lwZSo3EehT0dXggOjgtxFogpT3cZvoLGtIwrPBvm8jOQPXQUlaqCj10ThF1sYltEyukz/ovtDwYGew== + dependencies: + "@aws-sdk/client-sso" "3.363.0" + "@aws-sdk/token-providers" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/shared-ini-file-loader" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-provider-web-identity@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.363.0.tgz" + integrity sha512-Z6w7fjgy79pAax580wdixbStQw10xfyZ+hOYLcPudoYFKjoNx0NQBejg5SwBzCF/HQL23Ksm9kDfbXDX9fkPhA== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/credential-providers@^3.329.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.363.0.tgz" + integrity sha512-hVa1DdYasnLud2EKjDAlDHiV/+H/Zq52chHU00c/R8XwPu1s0kZX3NMmlt0D2HhYqC1mUwtdmE58Jra2POviQQ== + dependencies: + "@aws-sdk/client-cognito-identity" "3.363.0" + "@aws-sdk/client-sso" "3.363.0" + "@aws-sdk/client-sts" "3.363.0" + "@aws-sdk/credential-provider-cognito-identity" "3.363.0" + "@aws-sdk/credential-provider-env" "3.363.0" + "@aws-sdk/credential-provider-ini" "3.363.0" + "@aws-sdk/credential-provider-node" "3.363.0" + "@aws-sdk/credential-provider-process" "3.363.0" + "@aws-sdk/credential-provider-sso" "3.363.0" + "@aws-sdk/credential-provider-web-identity" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/credential-provider-imds" "^1.0.1" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/fetch-http-handler@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.357.0.tgz" + integrity sha512-5sPloTO8y8fAnS/6/Sfp/aVoL9zuhzkLdWBORNzMazdynVNEzWKWCPZ27RQpgkaCDHiXjqUY4kfuFXAGkvFfDQ== + dependencies: + "@aws-sdk/protocol-http" "3.357.0" + "@aws-sdk/querystring-builder" "3.357.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-base64" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/hash-blob-browser@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.357.0.tgz" + integrity sha512-RDd6UgrGHDmleTnXM9LRSSVa69euSAG2mlNhZMEDWk3OFseXVYqBDaqroVbQ01rM2UAe8MeBFchlV9OmxuVgvw== + dependencies: + "@aws-sdk/chunked-blob-reader" "3.310.0" + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/hash-stream-node@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.357.0.tgz" + integrity sha512-KZjN1VAw1KHNp+xKVOWBGS+MpaYQTjZFD5f+7QQqW4TfbAkFFwIAEYIHq5Q8Gw+jVh0h61OrV/LyW3J2PVzc+w== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-utf8" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/is-array-buffer@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.310.0.tgz" + integrity sha512-urnbcCR+h9NWUnmOtet/s4ghvzsidFmspfhYaHAmSRdy9yDjdjBJMFjjsn85A1ODUktztm+cVncXjQ38WCMjMQ== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/md5-js@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.357.0.tgz" + integrity sha512-to42sFAL7KgV/X9X40LLfEaNMHMGQL6/7mPMVCL/W2BZf3zw5OTl3lAaNyjXA+gO5Uo4lFEiQKAQVKNbr8b8Nw== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-utf8" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-bucket-endpoint@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.363.0.tgz" + integrity sha512-kR8+0X50zslpzRW29q4JbpPMadE1z39ZfGwPaBLKpoWvSGt4x+75FaoK71TH7urPPoFyD2Y+XKGA6YRYTUNHSQ== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-arn-parser" "3.310.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + "@smithy/util-config-provider" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/middleware-expect-continue@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.363.0.tgz" + integrity sha512-I88xneZp6jRwySmIl9uI7eZCcTsqRVnTDfUr1JiXt7zonqNNm80PVYMs6pwaw7t97ec1AQJcsONjuXZyCMnu5g== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-flexible-checksums@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.363.0.tgz" + integrity sha512-FBYmrMRX01uNximNN0WLgpf97GN4xNTLaKsDlkjYRWKJ+J97ICkvLG0FcSu7+SNCpCdJJBeQ5tRVOPVpUu6nmA== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@aws-crypto/crc32c" "3.0.0" + "@aws-sdk/types" "3.357.0" + "@smithy/is-array-buffer" "^1.0.1" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/middleware-host-header@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.363.0.tgz" + integrity sha512-FobpclDCf5Y1ueyJDmb9MqguAdPssNMlnqWQpujhYVABq69KHu73fSCWSauFPUrw7YOpV8kG1uagDF0POSxHzA== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-location-constraint@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.363.0.tgz" + integrity sha512-piNzpNNI/fChSGOZxcq/2msN2qFUSEAbhqs91zbcpv8CEPekVLc4W9laXCG764BEMyfG97ZU8MtzwHeMhELhBA== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-logger@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.363.0.tgz" + integrity sha512-SSGgthScYnFGTOw8EzbkvquqweFmvn7uJihkpFekbtBNGC/jGOGO+8ziHjTQ8t/iI/YKubEwv+LMi0f77HKSEg== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-recursion-detection@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.363.0.tgz" + integrity sha512-MWD/57QgI/N7fG8rtzDTUdSqNpYohQfgj9XCFAoVeI/bU4usrkOrew43L4smJG4XrDxlNT8lSJlDtd64tuiUZA== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-sdk-api-gateway@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-api-gateway/-/middleware-sdk-api-gateway-3.363.0.tgz" + integrity sha512-jW4gQwU94CGkBfrpPr5Biz/g2Q6XMcE7rBEBBLgDZw+528TRCcoCXlj2yrOsEbrvIzqms7+l0YsaTaOxAwRSKw== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-sdk-route53@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-route53/-/middleware-sdk-route53-3.363.0.tgz" + integrity sha512-t8jOjlvgkm5GFkj+RGRt/W9kTdmxztRzK26M85qmHbEzH430zJEzOpw1HKyOxuSvcrUWUdHfy/aR5Qqp0Vz0Mw== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-sdk-s3@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.363.0.tgz" + integrity sha512-npC8vLCero+vULizrK0QPjNanWbgH4A/2Llc1nO8N005uvUe7co6WglILF2W3guZrFk/0uGEdX67OnLxUD97pw== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-arn-parser" "3.310.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-sdk-sts@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.363.0.tgz" + integrity sha512-1yy2Ac50FO8BrODaw5bPWvVrRhaVLqXTFH6iHB+dJLPUkwtY5zLM3Mp+9Ilm7kME+r7oIB1wuO6ZB1Lf4ZszIw== + dependencies: + "@aws-sdk/middleware-signing" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-signing@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.363.0.tgz" + integrity sha512-/7qia715pt9JKYIPDGu22WmdZxD8cfF/5xB+1kmILg7ZtjO0pPuTaCNJ7xiIuFd7Dn7JXp5lop08anX/GOhNRQ== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/protocol-http" "^1.1.0" + "@smithy/signature-v4" "^1.0.1" + "@smithy/types" "^1.1.0" + "@smithy/util-middleware" "^1.0.1" + tslib "^2.5.0" + +"@aws-sdk/middleware-ssec@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.363.0.tgz" + integrity sha512-pN+QN1rMShYpJnTJSCIYnNRhD0S8xSZsTn6ThgcO559Xiwz5LMHFOfOXUCEyxtbVW5kMHLUh3w101AMUKae99A== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/middleware-stack@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.357.0.tgz" + integrity sha512-nNV+jfwGwmbOGZujAY/U8AW3EbVlxa9DJDLz3TPp/39o6Vu5KEzHJyDDNreo2k9V/TMvV+nOzHafufgPdagv7w== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/middleware-user-agent@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.363.0.tgz" + integrity sha512-ri8YaQvXP6odteVTMfxPqFR26Q0h9ejtqhUDv47P34FaKXedEM4nC6ix6o+5FEYj6l8syGyktftZ5O70NoEhug== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-endpoints" "3.357.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/node-config-provider@^3.329.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.357.0.tgz" + integrity sha512-kwBIzKCaW3UWqLdELhy7TcN8itNMOjbzga530nalFILMvn2IxrkdKQhNgxGBXy6QK6kCOtH6OmcrG3/oZkLwig== + dependencies: + "@aws-sdk/property-provider" "3.357.0" + "@aws-sdk/shared-ini-file-loader" "3.357.0" + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/node-http-handler@3.360.0": + version "3.360.0" + resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.360.0.tgz" + integrity sha512-oMsXdMmNwHpUbebETO44bq0N4SocEMGfPjYNUTRs8md7ita5fuFd2qFuvf+ZRt6iVcGWluIqmF8DidD+b7d+TA== + dependencies: + "@aws-sdk/abort-controller" "3.357.0" + "@aws-sdk/protocol-http" "3.357.0" + "@aws-sdk/querystring-builder" "3.357.0" + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/property-provider@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.357.0.tgz" + integrity sha512-im4W0u8WaYxG7J7ko4Xl3OEzK3Mrm1Rz6/txTGe6hTIHlyUISu1ekOQJXK6XYPqNMn8v1G3BiQREoRXUEJFbHg== + dependencies: + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/protocol-http@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.357.0.tgz" + integrity sha512-w1JHiI50VEea7duDeAspUiKJmmdIQblvRyjVMOqWA6FIQAyDVuEiPX7/MdQr0ScxhtRQxHbP0I4MFyl7ctRQvA== + dependencies: + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/querystring-builder@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.357.0.tgz" + integrity sha512-aQcicqB6Y2cNaXPPwunz612a01SMiQQPsdz632F/3Lzn0ua82BJKobHOtaiTUlmVJ5Q4/EAeNfwZgL7tTUNtDQ== + dependencies: + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-uri-escape" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/shared-ini-file-loader@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.357.0.tgz" + integrity sha512-ceyqM4XxQe0Plb/oQAD2t1UOV2Iy4PFe1oAGM8dfJzYrRKu7zvMwru7/WaB3NYq+/mIY6RU+jjhRmjQ3GySVqA== + dependencies: + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/signature-v4-multi-region@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.363.0.tgz" + integrity sha512-iWamQSpaBKg88LKuiUq8xO/7iyxJ+ORkA3qDhAwUqyTJOg87ma47yFf4ycCKqINnflc3AIGLGzBHnkBc4cMF5g== + dependencies: + "@aws-sdk/types" "3.357.0" + "@smithy/protocol-http" "^1.1.0" + "@smithy/signature-v4" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/smithy-client@^3.329.0": + version "3.360.0" + resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.360.0.tgz" + integrity sha512-R7wbT2SkgWNEAxMekOTNcPcvBszabW2+qHjrcelbbVJNjx/2yK+MbpZI4WRSncByQMeeoW+aSUP+JgsbpiOWfw== + dependencies: + "@aws-sdk/middleware-stack" "3.357.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-stream" "3.360.0" + "@smithy/types" "^1.0.0" + tslib "^2.5.0" + +"@aws-sdk/token-providers@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.363.0.tgz" + integrity sha512-6+0aJ1zugNgsMmhTtW2LBWxOVSaXCUk2q3xyTchSXkNzallYaRiZMRkieW+pKNntnu0g5H1T0zyfCO0tbXwxEA== + dependencies: + "@aws-sdk/client-sso-oidc" "3.363.0" + "@aws-sdk/types" "3.357.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/shared-ini-file-loader" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" + +"@aws-sdk/types@3.357.0", "@aws-sdk/types@^3.222.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.357.0.tgz" + integrity sha512-/riCRaXg3p71BeWnShrai0y0QTdXcouPSM0Cn1olZbzTf7s71aLEewrc96qFrL70XhY4XvnxMpqQh+r43XIL3g== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-arn-parser@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz" + integrity sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-base64@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.310.0.tgz" + integrity sha512-v3+HBKQvqgdzcbL+pFswlx5HQsd9L6ZTlyPVL2LS9nNXnCcR3XgGz9jRskikRUuUvUXtkSG1J88GAOnJ/apTPg== + dependencies: + "@aws-sdk/util-buffer-from" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/util-buffer-from@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.310.0.tgz" + integrity sha512-i6LVeXFtGih5Zs8enLrt+ExXY92QV25jtEnTKHsmlFqFAuL3VBeod6boeMXkN2p9lbSVVQ1sAOOYZOHYbYkntw== + dependencies: + "@aws-sdk/is-array-buffer" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/util-config-provider@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.310.0.tgz" + integrity sha512-xIBaYo8dwiojCw8vnUcIL4Z5tyfb1v3yjqyJKJWV/dqKUFOOS0U591plmXbM+M/QkXyML3ypon1f8+BoaDExrg== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-endpoints@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.357.0.tgz" + integrity sha512-XHKyS5JClT9su9hDif715jpZiWHQF9gKZXER8tW0gOizU3R9cyWc9EsJ2BRhFNhi7nt/JF/CLUEc5qDx3ETbUw== + dependencies: + "@aws-sdk/types" "3.357.0" + tslib "^2.5.0" + +"@aws-sdk/util-hex-encoding@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.310.0.tgz" + integrity sha512-sVN7mcCCDSJ67pI1ZMtk84SKGqyix6/0A1Ab163YKn+lFBQRMKexleZzpYzNGxYzmQS6VanP/cfU7NiLQOaSfA== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz" + integrity sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-middleware@3.357.0": + version "3.357.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.357.0.tgz" + integrity sha512-pV1krjZs7BdahZBfsCJMatE8kcor7GFsBOWrQgQDm9T0We5b5xPpOO2vxAD0RytBpY8Ky2ELs/+qXMv7l5fWIA== + dependencies: + tslib "^2.5.0" + +"@aws-sdk/util-stream@3.360.0": + version "3.360.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-stream/-/util-stream-3.360.0.tgz" + integrity sha512-t3naBfNesXwLis29pzSfLx2ifCn2180GiPjRaIsQP14IiVCBOeT1xaU6Dpyk7WeR/jW4cu7wGl+kbeyfNF6QmQ== + dependencies: + "@aws-sdk/fetch-http-handler" "3.357.0" + "@aws-sdk/node-http-handler" "3.360.0" + "@aws-sdk/types" "3.357.0" + "@aws-sdk/util-base64" "3.310.0" + "@aws-sdk/util-buffer-from" "3.310.0" + "@aws-sdk/util-hex-encoding" "3.310.0" + "@aws-sdk/util-utf8" "3.310.0" + tslib "^2.5.0" + +"@aws-sdk/util-uri-escape@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.310.0.tgz" + integrity sha512-drzt+aB2qo2LgtDoiy/3sVG8w63cgLkqFIa2NFlGpUgHFWTXkqtbgf4L5QdjRGKWhmZsnqkbtL7vkSWEcYDJ4Q== + dependencies: + tslib "^2.5.0" -"@hapi/h2o2@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@hapi/h2o2/-/h2o2-8.3.2.tgz#008a8f9ec3d9bba29077691aa9ec0ace93d4de80" - integrity sha512-2WkZq+QAkvYHWGqnUuG0stcVeGyv9T7bopBYnCJSUEuvBZlUf2BTX2JCVSKxsnTLOxCYwoC/aI4Rr0ZSRd2oVg== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/joi" "16.x.x" - "@hapi/wreck" "15.x.x" - -"@hapi/hapi@^18.4.1": - version "18.4.1" - resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-18.4.1.tgz#023fbc131074b1cb2cd7f6766d65f4b0e92df788" - integrity sha512-9HjVGa0Z4Qv9jk9AVoUdJMQLA+KuZ+liKWyEEkVBx3e3H1F0JM6aGbPkY9jRfwsITBWGBU2iXazn65SFKSi/tg== - dependencies: - "@hapi/accept" "^3.2.4" - "@hapi/ammo" "^3.1.2" - "@hapi/boom" "7.x.x" - "@hapi/bounce" "1.x.x" - "@hapi/call" "^5.1.3" - "@hapi/catbox" "10.x.x" - "@hapi/catbox-memory" "4.x.x" - "@hapi/heavy" "6.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/joi" "15.x.x" - "@hapi/mimos" "4.x.x" - "@hapi/podium" "3.x.x" - "@hapi/shot" "4.x.x" - "@hapi/somever" "2.x.x" - "@hapi/statehood" "6.x.x" - "@hapi/subtext" "^6.1.3" - "@hapi/teamwork" "3.x.x" - "@hapi/topo" "3.x.x" - -"@hapi/heavy@6.x.x": - version "6.2.2" - resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-6.2.2.tgz#d42a282c62d5bb6332e497d8ce9ba52f1609f3e6" - integrity sha512-PY1dCCO6dsze7RlafIRhTaGeyTgVe49A/lSkxbhKGjQ7x46o/OFf7hLiRqTCDh3atcEKf6362EaB3+kTUbCsVA== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/joi" "16.x.x" - -"@hapi/hoek@8.x.x", "@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0", "@hapi/hoek@^8.3.1": - version "8.5.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" - integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== - -"@hapi/iron@5.x.x": - version "5.1.4" - resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-5.1.4.tgz#7406f36847f798f52b92d1d97f855e27973832b7" - integrity sha512-+ElC+OCiwWLjlJBmm8ZEWjlfzTMQTdgPnU/TsoU5QsktspIWmWi9IU4kU83nH+X/SSya8TP8h8P11Wr5L7dkQQ== - dependencies: - "@hapi/b64" "4.x.x" - "@hapi/boom" "7.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/cryptiles" "4.x.x" - "@hapi/hoek" "8.x.x" - -"@hapi/joi@15.x.x": - version "15.1.1" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" - integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== - dependencies: - "@hapi/address" "2.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/topo" "3.x.x" - -"@hapi/joi@16.x.x": - version "16.1.8" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839" - integrity sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg== - dependencies: - "@hapi/address" "^2.1.2" - "@hapi/formula" "^1.2.0" - "@hapi/hoek" "^8.2.4" - "@hapi/pinpoint" "^1.0.2" - "@hapi/topo" "^3.1.3" - -"@hapi/mimos@4.x.x": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-4.1.1.tgz#4dab8ed5c64df0603c204c725963a5faa4687e8a" - integrity sha512-CXoi/zfcTWfKYX756eEea8rXJRIb9sR4d7VwyAH9d3BkDyNgAesZxvqIdm55npQc6S9mU3FExinMAQVlIkz0eA== +"@aws-sdk/util-user-agent-browser@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.363.0.tgz" + integrity sha512-fk9ymBUIYbxiGm99Cn+kAAXmvMCWTf/cHAcB79oCXV4ELXdPa9lN5xQhZRFNxLUeXG4OAMEuCAUUuZEj8Fnc1Q== dependencies: - "@hapi/hoek" "8.x.x" - mime-db "1.x.x" + "@aws-sdk/types" "3.357.0" + "@smithy/types" "^1.1.0" + bowser "^2.11.0" + tslib "^2.5.0" -"@hapi/nigel@3.x.x": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-3.1.1.tgz#84794021c9ee6e48e854fea9fb76e9f7e78c99ad" - integrity sha512-R9YWx4S8yu0gcCBrMUDCiEFm1SQT895dMlYoeNBp8I6YhF1BFF1iYPueKA2Kkp9BvyHdjmvrxCOns7GMmpl+Fw== +"@aws-sdk/util-user-agent-node@3.363.0": + version "3.363.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.363.0.tgz" + integrity sha512-Fli/dvgGA9hdnQUrYb1//wNSFlK2jAfdJcfNXA6SeBYzSeH5pVGYF4kXF0FCdnMA3Fef+Zn1zAP/hw9v8VJHWQ== dependencies: - "@hapi/hoek" "8.x.x" - "@hapi/vise" "3.x.x" + "@aws-sdk/types" "3.357.0" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@hapi/pez@^4.1.2": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-4.1.2.tgz#14984d0c31fed348f10c962968a21d9761f55503" - integrity sha512-8zSdJ8cZrJLFldTgwjU9Fb1JebID+aBCrCsycgqKYe0OZtM2r3Yv3aAwW5z97VsZWCROC1Vx6Mdn4rujh5Ktcg== +"@aws-sdk/util-utf8-browser@^3.0.0": + version "3.259.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz" + integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw== dependencies: - "@hapi/b64" "4.x.x" - "@hapi/boom" "7.x.x" - "@hapi/content" "^4.1.1" - "@hapi/hoek" "8.x.x" - "@hapi/nigel" "3.x.x" + tslib "^2.3.1" -"@hapi/pinpoint@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13" - integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ== - -"@hapi/podium@3.x.x": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-3.4.3.tgz#d28935870ae1372e2f983a7161e710c968a60de1" - integrity sha512-QJlnYLEYZWlKQ9fSOtuUcpANyoVGwT68GA9P0iQQCAetBK0fI+nbRBt58+aMixoifczWZUthuGkNjqKxgPh/CQ== +"@aws-sdk/util-utf8@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.310.0.tgz" + integrity sha512-DnLfFT8uCO22uOJc0pt0DsSNau1GTisngBCDw8jQuWT5CqogMJu4b/uXmwEqfj8B3GX6Xsz8zOd6JpRlPftQoA== dependencies: - "@hapi/hoek" "8.x.x" - "@hapi/joi" "16.x.x" + "@aws-sdk/util-buffer-from" "3.310.0" + tslib "^2.5.0" -"@hapi/shot@4.x.x": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-4.1.2.tgz#69f999956041fe468701a89a413175a521dabed5" - integrity sha512-6LeHLjvsq/bQ0R+fhEyr7mqExRGguNTrxFZf5DyKe3CK6pNabiGgYO4JVFaRrLZ3JyuhkS0fo8iiRE2Ql2oA/A== +"@aws-sdk/xml-builder@3.310.0": + version "3.310.0" + resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz" + integrity sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw== dependencies: - "@hapi/hoek" "8.x.x" - "@hapi/joi" "16.x.x" + tslib "^2.5.0" -"@hapi/somever@2.x.x": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-2.1.1.tgz#142bddf7cc4d829f678ed4e60618630a9a7ae845" - integrity sha512-cic5Sto4KGd9B0oQSdKTokju+rYhCbdpzbMb0EBnrH5Oc1z048hY8PaZ1lx2vBD7I/XIfTQVQetBH57fU51XRA== - dependencies: - "@hapi/bounce" "1.x.x" - "@hapi/hoek" "8.x.x" - -"@hapi/statehood@6.x.x": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-6.1.2.tgz#6dda508b5da99a28a3ed295c3cac795cf6c12a02" - integrity sha512-pYXw1x6npz/UfmtcpUhuMvdK5kuOGTKcJNfLqdNptzietK2UZH5RzNJSlv5bDHeSmordFM3kGItcuQWX2lj2nQ== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/bounce" "1.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/cryptiles" "4.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/iron" "5.x.x" - "@hapi/joi" "16.x.x" - -"@hapi/subtext@^6.1.3": - version "6.1.3" - resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-6.1.3.tgz#bbd07771ae2a4e73ac360c93ed74ac641718b9c6" - integrity sha512-qWN6NbiHNzohVcJMeAlpku/vzbyH4zIpnnMPMPioQMwIxbPFKeNViDCNI6fVBbMPBiw/xB4FjqiJkRG5P9eWWg== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/content" "^4.1.1" - "@hapi/file" "1.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/pez" "^4.1.2" - "@hapi/wreck" "15.x.x" - -"@hapi/teamwork@3.x.x": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-3.3.1.tgz#b52d0ec48682dc793926bd432e22ceb19c915d3f" - integrity sha512-61tiqWCYvMKP7fCTXy0M4VE6uNIwA0qvgFoiDubgfj7uqJ0fdHJFQNnVPGrxhLWlwz0uBPWrQlBH7r8y9vFITQ== - -"@hapi/topo@3.x.x", "@hapi/topo@^3.1.3": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" - integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== +"@babel/runtime@^7.3.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz" + integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA== dependencies: - "@hapi/hoek" "^8.3.0" + regenerator-runtime "^0.13.4" -"@hapi/vise@3.x.x": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-3.1.1.tgz#dfc88f2ac90682f48bdc1b3f9b8f1eab4eabe0c8" - integrity sha512-OXarbiCSadvtg+bSdVPqu31Z1JoBL+FwNYz3cYoBKQ5xq1/Cr4A3IkGpAZbAuxU5y4NL5pZFZG3d2a3ZGm/dOQ== +"@cloudcmd/copy-file@^1.1.0": + version "1.1.1" + resolved "https://registry.npmjs.org/@cloudcmd/copy-file/-/copy-file-1.1.1.tgz" + integrity sha512-t6pTJdsV0qhh9YX22/Npsv95GqVABc5GRInSK7JSSNIpPLq9TM+K7odYzcOuQRPZAD9OHxZfbYsB4WJOalzqng== dependencies: - "@hapi/hoek" "8.x.x" + es6-promisify "^6.0.0" + pipe-io "^3.0.0" + wraptile "^2.0.0" + zames "^2.0.0" -"@hapi/wreck@15.x.x": - version "15.1.0" - resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-15.1.0.tgz#7917cd25950ce9b023f7fd2bea6e2ef72c71e59d" - integrity sha512-tQczYRTTeYBmvhsek/D49En/5khcShaBEmzrAaDjMrFXKJRuF8xA8+tlq1ETLBFwGd6Do6g2OC74rt11kzawzg== - dependencies: - "@hapi/boom" "7.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/hoek" "8.x.x" +"@iarna/toml@^2.2.5": + version "2.2.5" + resolved "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz" + integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== "@kwsites/file-exists@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" + resolved "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz" integrity sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== dependencies: debug "^4.1.1" "@kwsites/promise-deferred@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" + resolved "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz" integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== "@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" @@ -336,747 +1125,666 @@ "@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@^6.2.3": + version "6.2.3" + resolved "https://registry.npmjs.org/@serverless/dashboard-plugin/-/dashboard-plugin-6.2.3.tgz" + integrity sha512-iTZhpZbiVl6G2AyfgoqxemqqpG4pUceWys3GsyZtjimnfnGd2UFBOMVUMTavLhYia7lQc4kQVuXQ+afLlkg+pQ== dependencies: "@serverless/event-mocks" "^1.1.1" - "@serverless/platform-client" "^3.1.5" - "@serverless/platform-sdk" "^2.3.2" - chalk "^4.1.0" + "@serverless/platform-client" "^4.3.2" + "@serverless/utils" "^6.8.2" child-process-ext "^2.1.1" - chokidar "^3.5.0" - cli-color "^2.0.0" + 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" + type "^2.7.2" uuid "^8.3.2" yamljs "^0.3.0" "@serverless/event-mocks@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@serverless/event-mocks/-/event-mocks-1.1.1.tgz#7064b99ccc29d9a8e9b799f413dbcfd64ea3b7ee" + resolved "https://registry.npmjs.org/@serverless/event-mocks/-/event-mocks-1.1.1.tgz" integrity sha512-YAV5V/y+XIOfd+HEVeXfPWZb8C6QLruFk9tBivoX2roQLWVq145s4uxf8D0QioCueuRzkukHUS4JIj+KVoS34A== dependencies: "@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.3.2": + version "4.5.1" + resolved "https://registry.npmjs.org/@serverless/platform-client/-/platform-client-4.5.1.tgz" + 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", "@serverless/utils@^6.11.1", "@serverless/utils@^6.8.2": + version "6.11.2" + resolved "https://registry.npmjs.org/@serverless/utils/-/utils-6.11.2.tgz" + integrity sha512-Uww5DM78K+bHmukNgVX3Yieu7CVnOKvpUhxxRe+5WiYBV7mNrLiZr9bNAtUSNOYFS4tU5Ig5YlMCCForCCYxEw== 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== - 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 "^3.1.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.6.0" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== -"@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@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-1.0.1.tgz" + integrity sha512-An6irzp9NCji2JtJHhrEFlDbxLwHd6c6Y9fq3ZeomyUR8BIXlGXVTxsemUSZVVgOq3166iYbYs/CrPAmgRSFLw== 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" "^1.1.0" + tslib "^2.5.0" -"@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/config-resolver@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-1.0.1.tgz" + integrity sha512-quj0xUiEVG/UHfY82EtthR/+S5/17p3IxXArC3NFSNqryMobWbG9oWgJy2s2cgUSVZLzxevjKKvxrilK7JEDaA== 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" - -"@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/types" "^1.1.0" + "@smithy/util-config-provider" "^1.0.1" + "@smithy/util-middleware" "^1.0.1" + tslib "^2.5.0" -"@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/credential-provider-imds@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-1.0.1.tgz" + integrity sha512-hkRJoxVCh4CEt1zYOBElE+G/MV6lyx3g68hSJpesM4pwMT/bzEVo5E5XzXY+6dVq8yszeatWKbFuqCCBQte8tg== + dependencies: + "@smithy/node-config-provider" "^1.0.1" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + "@smithy/url-parser" "^1.0.1" + tslib "^2.5.0" -"@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/eventstream-codec@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-1.0.1.tgz" + integrity sha512-cpcTXQEOEs2wEvIyxW/iTHJ2m0RVqoEOTjjWEXD6SY8Gcs3FCFP6E8MXadC098tdH5ctMIUXc8POXyMpxzGnjw== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@smithy/types" "^1.1.0" + "@smithy/util-hex-encoding" "^1.0.1" + tslib "^2.5.0" -"@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-serde-browser@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-1.0.1.tgz" + integrity sha512-oc8vxe+AU2RzvXH/Ehh0TzM/Nsw3I3ywu7V3qaCzqdkBIntAwK9JGZqcSDsqTK0WxZKBRgFIEwopcuZ2slVnFQ== dependencies: - defer-to-connect "^1.0.1" + "@smithy/eventstream-serde-universal" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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-config-resolver@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-1.0.1.tgz" + integrity sha512-TJwaXima0djnNY819utO1j93qZHaheFH1bhHxBkMrImtEOuXY48Tjma/L2m8swkIq8dy8jFC9hrYOkD0eYHkFA== dependencies: - defer-to-connect "^2.0.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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-node@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-1.0.1.tgz" + integrity sha512-JEj8w7IRs4l+kcwKxbv3pNuu8n7ORC4pMFrIOrM4rERzrRnI7vMNTRzvAPGYA53rqm/Y9tBA9dw4C+H6hLXcsA== dependencies: - "@types/request" "^2.48.3" - "@types/request-promise-native" "^1.0.17" - request "^2.88.0" - request-promise-native "^1.0.8" + "@smithy/eventstream-serde-universal" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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-universal@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-1.0.1.tgz" + integrity sha512-c6m9DH7m6D2S93dof4wSxysaGSQdauO20TNcSePzrgHd4rkTnz5pqZ1a7Pt22q2SKf09SvTugq5cV2Sy4r8zHw== dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "*" - "@types/node" "*" - "@types/responselike" "*" + "@smithy/eventstream-codec" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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/fetch-http-handler@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-1.0.1.tgz" + integrity sha512-/e2A8eOMk4FVZBQ0o6uF/ttLtFZcmsK5MIwDu1UE3crM4pCAIP19Ul8U9rdLlHhIu81X4AcJmSw55RDSpVRL/w== + dependencies: + "@smithy/protocol-http" "^1.1.0" + "@smithy/querystring-builder" "^1.0.1" + "@smithy/types" "^1.1.0" + "@smithy/util-base64" "^1.0.1" + tslib "^2.5.0" -"@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/hash-node@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-1.0.1.tgz" + integrity sha512-eCz08BySBcOjVObjbRAS/XMKUGY4ujnuS+GoWeEpzpCSKDnO8/YQ0rStRt4C0llRmhApizYc1tK9DiJwfvXcBg== + dependencies: + "@smithy/types" "^1.1.0" + "@smithy/util-buffer-from" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" -"@types/keyv@*": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" - integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== +"@smithy/invalid-dependency@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-1.0.1.tgz" + integrity sha512-kib63GFlAzRn/wf8M0cRWrZA1cyOy5IvpTkLavCY782DPFMP0EaEeD6VrlNIOvD6ncf7uCJ68HqckhwK1qLT3g== dependencies: - "@types/node" "*" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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/is-array-buffer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-1.0.1.tgz" + integrity sha512-fHSTW70gANnzPYWNDcWkPXpp+QMbHhKozbQm/+Denkhp4gwSiPuAovWZRpJa9sXO+Q4dOnNzYN2max1vTCEroA== + dependencies: + tslib "^2.5.0" -"@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/middleware-content-length@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-1.0.1.tgz" + integrity sha512-vWWigayk5i2cFp9xPX5vdzHyK+P0t/xZ3Ovp4Ss+c8JQ1Hlq2kpJZVWtTKsmdfND5rVo5lu0kD5wgAMUCcmuhw== + dependencies: + "@smithy/protocol-http" "^1.1.0" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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/middleware-endpoint@^1.0.1": + version "1.0.2" + resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-1.0.2.tgz" + integrity sha512-F3CyXgjtDI4quGFkDmVNytt6KMwlzzeMxtopk6Edue4uKdKcMC1vUmoRS5xTbFzKDDp4XwpnEV7FshPaL3eCPw== + dependencies: + "@smithy/middleware-serde" "^1.0.1" + "@smithy/types" "^1.1.0" + "@smithy/url-parser" "^1.0.1" + "@smithy/util-middleware" "^1.0.1" + tslib "^2.5.0" -"@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/middleware-retry@^1.0.1", "@smithy/middleware-retry@^1.0.2": + version "1.0.3" + resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-1.0.3.tgz" + integrity sha512-ZRsjG8adtxQ456FULPqPFmWtrW44Fq8IgdQvQB+rC2RSho3OUzS+TiEIwb5Zs6rf2IoewITKtfdtsUZcxXO0ng== + dependencies: + "@smithy/protocol-http" "^1.1.0" + "@smithy/service-error-classification" "^1.0.2" + "@smithy/types" "^1.1.0" + "@smithy/util-middleware" "^1.0.1" + "@smithy/util-retry" "^1.0.3" + tslib "^2.5.0" + uuid "^8.3.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/middleware-serde@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-1.0.1.tgz" + integrity sha512-bn5lWk8UUeXFCQfkrNErz5SbeNd+2hgYegHMLsOLPt4URDIsyREar6wMsdsR+8UCdgR5s8udG3Zalgc7puizIQ== dependencies: - "@types/request" "*" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@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-stack@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-1.0.1.tgz" + integrity sha512-T6+gsAO1JYamOJqmORCrByDeQ/NB+ggjHb33UDOgdX4xIjXz/FB/3UqHgQu6PL1cSFrK+i4oteDIwqARDs/Szw== dependencies: - "@types/caseless" "*" - "@types/node" "*" - "@types/tough-cookie" "*" - form-data "^2.5.0" + tslib "^2.5.0" -"@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/node-config-provider@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-1.0.1.tgz" + integrity sha512-FRxifH/J2SgOaVLihIqBFuGhiHR/NfzbZYp5nYO7BGgT/gc/f9nAuuRJcEy/hwO3aI6ThyG5apH4tGec6A2sCw== dependencies: - "@types/node" "*" + "@smithy/property-provider" "^1.0.1" + "@smithy/shared-ini-file-loader" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -"@types/retry@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@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/node-http-handler@^1.0.1", "@smithy/node-http-handler@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-1.0.2.tgz" + integrity sha512-PzPrGRSt3kNuruLCeR4ffJp57ZLVnIukMXVL3Ppr65ZoxiE+HBsOVAa/Z/T+4HzjCM6RaXnnmB8YKfsDjlb0iA== + dependencies: + "@smithy/abort-controller" "^1.0.1" + "@smithy/protocol-http" "^1.1.0" + "@smithy/querystring-builder" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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/property-provider@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-1.0.1.tgz" + integrity sha512-3EG/61Ls1MrgEaafpltXBJHSqFPqmTzEX7QKO7lOEHuYGmGYzZ08t1SsTgd1vM74z0IihoZyGPynZ7WmXKvTeg== + dependencies: + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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/protocol-http@^1.0.1", "@smithy/protocol-http@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-1.1.0.tgz" + integrity sha512-H5y/kZOqfJSqRkwtcAoVbqONmhdXwSgYNJ1Glk5Ry8qlhVVy5qUzD9EklaCH8/XLnoCsLO/F/Giee8MIvaBRkg== + dependencies: + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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/querystring-builder@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-1.0.1.tgz" + integrity sha512-J5Tzkw1PMtu01h6wl+tlN5vsyROmS6/z5lEfNlLo/L4ELHeVkQ4Q0PEIjDddPLfjVLCm8biQTESE5GCMixSRNQ== + dependencies: + "@smithy/types" "^1.1.0" + "@smithy/util-uri-escape" "^1.0.1" + tslib "^2.5.0" -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/querystring-parser@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-1.0.1.tgz" + integrity sha512-zauxdMc3cwxoLitI5DZqH7xN6Fk0mwRxrUMAETbav2j6Se2U0UGak/55rZcDg2yGzOURaLYi5iOm1gHr98P+Bw== dependencies: - debug "4" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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/service-error-classification@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-1.0.2.tgz" + integrity sha512-Q5CCuzYL5FGo6Rr/O+lZxXHm2hrRgbmMn8MgyjqZUWZg20COg20DuNtIbho2iht6CoB7jOpmpBqhWizLlzUZgg== -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/shared-ini-file-loader@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-1.0.1.tgz" + integrity sha512-EztziuIPoNronENGqh+MWVKJErA4rJpaPzJCPukzBeEoG2USka0/q4B5Mr/1zszOnrb49fPNh4u3u5LfiH7QzA== 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" "^1.1.0" + tslib "^2.5.0" -ansi-align@^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== +"@smithy/signature-v4@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-1.0.1.tgz" + integrity sha512-2D69je14ou1vBTnAQeysSK4QVMm0j3WHS3MDg/DnHnFFcXRCzVl/xAARO7POD8+fpi4tMFPs8Z4hzo1Zw40L0Q== + dependencies: + "@smithy/eventstream-codec" "^1.0.1" + "@smithy/is-array-buffer" "^1.0.1" + "@smithy/types" "^1.1.0" + "@smithy/util-hex-encoding" "^1.0.1" + "@smithy/util-middleware" "^1.0.1" + "@smithy/util-uri-escape" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@smithy/smithy-client@^1.0.2", "@smithy/smithy-client@^1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-1.0.3.tgz" + integrity sha512-Wh1mNP/1yUZK0uYkgCQ6NMxpBT3Fmc45TMdUfOlH1xD2zGYL7U4yDHFOhEZdi/suyjaelFobXB2p9pPIw6LjRQ== dependencies: - string-width "^3.0.0" + "@smithy/middleware-stack" "^1.0.1" + "@smithy/types" "^1.1.0" + "@smithy/util-stream" "^1.0.1" + tslib "^2.5.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/types@^1.0.0", "@smithy/types@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@smithy/types/-/types-1.1.0.tgz" + integrity sha512-KzmvisMmuwD2jZXuC9e65JrgsZM97y5NpDU7g347oB+Q+xQLU6hQZ5zFNNbEfwwOJHoOvEVTna+dk1h/lW7alw== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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/url-parser@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-1.0.1.tgz" + integrity sha512-33vWEtE6HzmwjEcEb4I58XMLRAchwPS93YhfDyXAXr1jwDCzfXmMayQwwpyW847rpWj0XJimxqia8q0z+k/ybw== dependencies: - ansi-wrap "0.1.0" + "@smithy/querystring-parser" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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/util-base64@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-1.0.1.tgz" + integrity sha512-rJcpRi/yUi6TyCEkjdTH86/ExBuKlfctEXhG9/4gMJ3/cnPcHJJnr0mQ9evSEO+3DbpT/Nxq90bcTBdTIAmCig== dependencies: - ansi-wrap "0.1.0" + "@smithy/util-buffer-from" "^1.0.1" + tslib "^2.5.0" -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/util-body-length-browser@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-1.0.1.tgz" + integrity sha512-Pdp744fmF7E1NWoSb7256Anhm8eYoCubvosdMwXzOnHuPRVbDa15pKUz2027K3+jrfGpXo1r+MnDerajME1Osw== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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-body-length-node@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-1.0.1.tgz" + integrity sha512-4PIHjDFwG07SNensAiVq/CJmubEVuwclWSYOTNtzBNTvxOeGLznvygkGYgPzS3erByT8C4S9JvnLYgtrsVV3nQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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-buffer-from@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-1.0.1.tgz" + integrity sha512-363N7Wq0ceUgE5lLe6kaR6GlJs2/m4r9V6bRMfIszb6P1FZbbRRM2FQYUWWPFSsRymm9mJL18b3fjiVsIvhDGg== dependencies: - ansi-wrap "0.1.0" + "@smithy/is-array-buffer" "^1.0.1" + tslib "^2.5.0" -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-config-provider@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-1.0.1.tgz" + integrity sha512-4Qy38Oy5/q43MpTwCLV1P+7NeaOp4W2etQDxMjgEeRlOyGGNlgttn0syi4g2rVSukFVqQ6FbeRs5xbnFmS6kaQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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-defaults-mode-browser@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-1.0.1.tgz" + integrity sha512-/9ObwNch4Z/NJYfkO4AvqBWku60Ju+c2Ck32toPOLmWe/V6eI9FLn8C1abri+GxDRCkLIqvkaWU1lgZ3nWZIIw== dependencies: - ansi-wrap "0.1.0" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + bowser "^2.11.0" + tslib "^2.5.0" -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-defaults-mode-node@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-1.0.1.tgz" + integrity sha512-XQM3KvqRLgv7bwAzVkXTITkOmcOINoG9icJiGT8FA0zV35lY5UvyIsg5kHw01xigQS8ufa/33AwG3ZoXip+V5g== dependencies: - ansi-wrap "0.1.0" + "@smithy/config-resolver" "^1.0.1" + "@smithy/credential-provider-imds" "^1.0.1" + "@smithy/node-config-provider" "^1.0.1" + "@smithy/property-provider" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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-hex-encoding@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-1.0.1.tgz" + integrity sha512-FPTtMz/t02/rbfq5Pdll/TWUYP+GVFLCQNr+DgifrLzVRU0g8rdRjyFpDh8nPTdkDDusTTo9P1bepAYj68s0eA== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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-middleware@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-1.0.1.tgz" + integrity sha512-u9akN3Zmbr0vZH4F+2iehG7cFg+3fvDfnvS/hhsXH4UHuhqiQ+ADefibnLzPoz1pooY7rvwaQ/TVHyJmZHdLdQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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-retry@^1.0.1", "@smithy/util-retry@^1.0.2", "@smithy/util-retry@^1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-1.0.3.tgz" + integrity sha512-gYQnZDD8I2XJFspVwUISyukjPWVikTzKR0IdG8hCWYPTpeULFl1o6yzXlT5SL63TBkuEYl0R1/93cdNtMiNnoA== dependencies: - ansi-wrap "0.1.0" + "@smithy/service-error-classification" "^1.0.2" + tslib "^2.5.0" -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-stream@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-1.0.1.tgz" + integrity sha512-4aBCIz35aZAnt2Rbq341KrnUzGhWv2/Zu8HouJqYLvSWCzlrvsNCGlXP4e70Kjzcw8hSuuCNtdUICwQ5qUWLxg== + dependencies: + "@smithy/fetch-http-handler" "^1.0.1" + "@smithy/node-http-handler" "^1.0.2" + "@smithy/types" "^1.1.0" + "@smithy/util-base64" "^1.0.1" + "@smithy/util-buffer-from" "^1.0.1" + "@smithy/util-hex-encoding" "^1.0.1" + "@smithy/util-utf8" "^1.0.1" + tslib "^2.5.0" + +"@smithy/util-uri-escape@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-1.0.1.tgz" + integrity sha512-IJUrRnXKEIc+PKnU1XzTsIENVR+60jUDPBP3iWX/EvuuT3Xfob47x1FGUe2c3yMXNuU6ax8VDk27hL5LKNoehQ== dependencies: - ansi-wrap "0.1.0" + tslib "^2.5.0" -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-utf8@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-1.0.1.tgz" + integrity sha512-iX6XHpjh4DFEUIBSKp2tjy3pYnLQMsJ62zYi1BVAC0kobE6p8AVpiZnxsU3ZkgQatAsUaEspFHUZ7CL7oSqaPQ== + dependencies: + "@smithy/util-buffer-from" "^1.0.1" + tslib "^2.5.0" -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-waiter@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-1.0.1.tgz" + integrity sha512-dsn8O0s3pFIgxFzySLe1dv0w4tEQizEP6UqbgZ4r/Kar4n8pSdrPi6DJg8BzXwkwEIZpMtV4/nhSeGZ7HksDXA== dependencies: - type-fest "^0.11.0" + "@smithy/abort-controller" "^1.0.1" + "@smithy/types" "^1.1.0" + tslib "^2.5.0" -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= +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== dependencies: - ansi-wrap "0.1.0" + defer-to-connect "^2.0.0" -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= +"@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.3" + resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== dependencies: - ansi-wrap "0.1.0" + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" -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= +"@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-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= +"@types/http-cache-semantics@*": + version "4.0.1" + resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz" + integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + +"@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-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/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== dependencies: - ansi-wrap "0.1.0" + "@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/lodash-es@^4.17.8": + version "4.17.12" + resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== dependencies: - ansi-wrap "0.1.0" + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.123": + version "4.14.191" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + +"@types/node@*": + version "18.14.2" + resolved "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz" + integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== -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/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: - 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= - dependencies: - ansi-wrap "0.1.0" +"@types/semver@^7.5.0": + version "7.5.0" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== -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/yarnpkg__lockfile@^1.1.6": + version "1.1.6" + resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.6.tgz" + integrity sha512-kbdQa3J+hVCkqmGQm31fthEwGxszZtepw84p9QGCiJB7TmiPqPAf3/g9eZUnkCeanmiFOaG4pVhiPDyqJxaoaw== -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +"@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== -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.10" + resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz" + integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== -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.1" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz" + integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + 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.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + 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.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +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: - ansi-wrap "0.1.0" + 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" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" 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.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" appdirectory@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/appdirectory/-/appdirectory-0.1.0.tgz#eb6c816320e7b2ab16f5ed997f28d8205df56375" + resolved "https://registry.npmjs.org/appdirectory/-/appdirectory-0.1.0.tgz" integrity sha1-62yBYyDnsqsW9e2ZfyjYIF31Y3U= -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" @@ -1092,7 +1800,7 @@ archiver-utils@^2.1.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" @@ -1103,346 +1811,210 @@ 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.1" + resolved "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz" + integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== dependencies: archiver-utils "^2.1.0" - async "^3.2.0" + async "^3.2.3" 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" + tar-stream "^2.2.0" + zip-stream "^4.1.0" argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" 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-swap@^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= - dependencies: - is-number "^3.0.0" +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== array-union@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" 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== +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.3: + version "3.2.4" + resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== async@~1.5: version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + resolved "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 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" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -aws-sdk@^2.624.0: - version "2.774.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.774.0.tgz#1d9512ae42f0cfb9b98d0d6e0d7df7634cf4e680" - integrity sha512-3a/fM1E3nCPwT4AVbysOWCMmsu/TOdJDD3urjywWE/qO1JShxRwLSdRLD1xRkacR9JcnydfkmdU0qk+VsM3nqg== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - -aws-sdk@^2.756.0: - version "2.787.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.787.0.tgz#4d8966d11c7dbe770de26632e552c97b2d91e340" - integrity sha512-3WlUdWqUB8Vhdvj/7TENr/7SEmQzxmnHxOJ8l2WjZbcMRSuI0/9Ym4p1TC3hf21VDVDhkdGlw60QqpZQ1qb+Mg== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -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== +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" - -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== + util "^0.12.4" + uuid "8.0.0" + xml2js "0.6.2" -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" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" 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= - 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.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 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.0.6, 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@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" - integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^3.0.0" - cli-boxes "^2.2.0" - string-width "^4.1.0" - term-size "^2.1.0" - type-fest "^0.8.1" - widest-line "^3.1.0" - -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.npmjs.org/bowser/-/bowser-2.11.0.tgz" + 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" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== +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" - 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== +braces@^3.0.1, braces@^3.0.2, 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-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - 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" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz" integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== dependencies: base64-js "^1.0.2" @@ -1451,103 +2023,72 @@ 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.npmjs.org/builtins/-/builtins-1.0.3.tgz" + 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" - 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" + version "5.0.4" + resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== -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" + resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -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= +call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= +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: - traverse ">=0.3.0 <0.4" + 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" + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -chalk@^2.0.1, chalk@^2.4.2: +chalk@^2.4.1: version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -1556,28 +2097,33 @@ chalk@^2.0.1, chalk@^2.4.2: chalk@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + 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.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@^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.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + 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: version "2.1.1" - resolved "https://registry.yarnpkg.com/child-process-ext/-/child-process-ext-2.1.1.tgz#f7cf4e68fef60c4c8ee911e1b402413191467dc3" + resolved "https://registry.npmjs.org/child-process-ext/-/child-process-ext-2.1.1.tgz" integrity sha512-0UQ55f51JBkOFa+fvR76ywRzxiPwQS3Xe8oe5bZRphpv+dIMeerW5Zn5e4cUy4COJwVtJyU0R79RMnw+aCqmGA== dependencies: cross-spawn "^6.0.5" @@ -1586,252 +2132,167 @@ 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== - 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" - -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.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 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" + resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -cli-boxes@^2.2.0, 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-spinners@^2.5.0: + version "2.7.0" + resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz" + integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== + +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: + cli-color "^2.0.1" + es5-ext "^0.10.53" + sprintf-kit "^2.0.1" + supports-color "^6.1.0" cli-width@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== cliui@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -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== - dependencies: - for-own "^1.0.0" - is-plain-object "^2.0.4" - kind-of "^5.0.0" - shallow-clone "^1.0.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" - -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.npmjs.org/clone-response/-/clone-response-1.0.3.tgz" + 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" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" 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" 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= + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + 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.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@^11.0.0: + version "11.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== -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" + resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" 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= - -component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@~1.3.0: +component-emitter@^1.2.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= - 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" @@ -1839,120 +2300,76 @@ 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.0: + version "4.1.1" + resolved "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz" + integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== 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= - -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -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= - -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.0, 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" + resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz" integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== dependencies: exit-on-epipe "~1.0.1" printj "~1.1.0" crc32-stream@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85" - 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== + version "3.0.1" + 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.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.2" + resolved "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz" + integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== 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" -cron-parser@^2.7.3: - version "2.16.3" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.16.3.tgz#acb8e405eed1733aac542fdf604cb7c1daf0204a" - integrity sha512-XNJBD1QLFeAMUkZtZQuncAAOgJFWNhBdIbwgD22hZxrcWOImBFMKgPC66GzaXpyoJs7UvYLLgPH/8BRk/7gbZg== +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: - is-nan "^1.3.0" - moment-timezone "^0.5.31" + 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" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== dependencies: nice-try "^1.0.4" @@ -1961,112 +2378,53 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0: - 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" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -cuid@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/cuid/-/cuid-2.1.8.tgz#cbb88f954171e0d5747606c0139fb65c5101eac0" - integrity sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg== - 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= - 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== +dayjs@^1.11.8: + version "1.11.9" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== -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== +debug@4, debug@^4.1.1, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -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== - dependencies: - ms "2.0.0" - -debug@^3.0.1, debug@^3.1.0, debug@^3.1.1: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - 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" - decamelize@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -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= - dependencies: - mimic-response "^1.0.0" + resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== 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" @@ -2075,7 +2433,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" @@ -2086,7 +2444,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" @@ -2095,8 +2453,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" @@ -2105,7 +2463,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" @@ -2117,24 +2475,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" - integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + version "2.0.1" + resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== 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" @@ -2143,266 +2498,97 @@ deferred@^0.7.11: next-tick "^1.0.0" timers-ext "^0.1.7" -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -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= - dependencies: - is-descriptor "^0.1.0" - -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= +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 "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" -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== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" +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== delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" 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= - -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== - 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= - 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-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.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.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz" + 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.3.1" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== 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" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -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== - dependencies: - after "0.8.2" - arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.4" - blob "0.0.5" - has-binary2 "~1.0.2" - -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== - -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= +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.17.0-next.1, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.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== +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: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + get-intrinsic "^1.2.4" + +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== -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== +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" - next-tick "~1.0.0" + 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" @@ -2411,39 +2597,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" @@ -2451,254 +2630,189 @@ es6-weak-map@^2.0.2, es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - 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" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" 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.npmjs.org/essentials/-/essentials-1.2.0.tgz" + 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" -eventemitter3@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - events@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - -execa@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2" - integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" + resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== 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" + resolved "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz" 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== - 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" sort-keys-length "^1.0.0" -ext@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== +ext@^1.1.2, 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.0.0" + 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, extend@~3.0.2: +extend@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" 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: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" 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.7, fast-glob@^3.2.9, fast-glob@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== 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-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-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.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== +fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz" + 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.0.7" + resolved "https://registry.npmjs.org/filesize/-/filesize-10.0.7.tgz" + integrity sha512-iMRG7Qo9nayLoU3PNCiLizYtsy4W1ClrapeCwEgtiQelOAOuRJiw4QaLI+sSr8xr901dgHv+EYP2bCusGZgoiA== -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-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" @@ -2706,7 +2820,7 @@ find-requires@^1.0.0: find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -2714,896 +2828,581 @@ find-up@^4.1.0: flat@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" 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= +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: + debug "^4.1.1" + graceful-fs "~4.2.0" + minimatch "~3.0.4" -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= +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== -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= +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== dependencies: - for-in "^1.0.1" - -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= + is-callable "^1.1.3" -form-data@^2.3.1, form-data@^2.5.0: +form-data@^2.3.1: version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== dependencies: asynckit "^0.4.0" combined-stream "^1.0.6" mime-types "^2.1.12" -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" + resolved "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz" 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= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.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.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.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + 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.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.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.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + 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" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: minipass "^3.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.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 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= - 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" +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== get-caller-file@^2.0.1: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.3: + version "1.2.0" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.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: + 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" + resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz" 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.0.0, get-stream@^5.1.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" - -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-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== -glob-all@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.2.1.tgz#082ca81afd2247cbd3ed2149bb2630f4dc877d95" - integrity sha512-x877rVkzB3ipid577QOp+eQCR6M5ZyiwrtaYgrX/z3EThaSPFtLDwBXFHc3sH1cG0R0vFYI5SRYeWMMSEyXkUw== +glob-all@^3.3.0: + version "3.3.1" + resolved "https://registry.npmjs.org/glob-all/-/glob-all-3.3.1.tgz" + integrity sha512-Y+ESjdI7ZgMwfzanHZYQ87C59jOO0i+Hd+QYtVt9PhLi6d8wlOpzQnfBxWUlaTuAoR3TkybLqqbIoWveU4Ji7Q== dependencies: - glob "^7.1.2" + glob "^7.2.3" yargs "^15.3.1" -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.2, glob@^7.1.3, glob@^7.1.4: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" - integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== - dependencies: - ini "^1.3.5" - -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== +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + 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== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" + 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" + resolved "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz" 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-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-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -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.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: + 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-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" 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" hasbin@^1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/hasbin/-/hasbin-1.2.3.tgz#78c5926893c80215c2b568ae1fd3fcab7a2696b0" + resolved "https://registry.npmjs.org/hasbin/-/hasbin-1.2.3.tgz" integrity sha1-eMWSaJPIAhXCtWiuH9P8q3omlrA= - dependencies: - async "~1.5" - -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== - -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== + dependencies: + async "~1.5" -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= +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + function-bind "^1.1.2" + +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" - integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== + version "1.0.3" + resolved "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== 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.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - -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= - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" + 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.5, 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= - dependencies: - from2 "^2.1.1" - p-is-promise "^1.1.0" - -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= - dependencies: - kind-of "^3.0.2" - -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-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== dependencies: - kind-of "^6.0.0" - -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== + call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 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-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -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= - dependencies: - kind-of "^3.0.2" - -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-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -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== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -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== - 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= +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== 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" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz" integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== -is-extendable@^0.1.0, is-extendable@^0.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: 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.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, 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-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - -is-nan@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" - integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ== - dependencies: - define-properties "^1.1.3" +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= - -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-npm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" - integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== - -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-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== + resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== 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-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -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-path-inside@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== - 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" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== 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.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz" + 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-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -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-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-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== +is-typed-array@^1.1.10, is-typed-array@^1.1.3: + version "1.1.10" + resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== dependencies: - has-symbols "^1.0.1" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" -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-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-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-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - 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" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= 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= + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + 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" + resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz" 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" - -java-invoke-local@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/java-invoke-local/-/java-invoke-local-0.0.6.tgz#0e04b20b5e306a1e8384846a9ac286790ee6d868" - integrity sha512-gZmQKe1QrfkkMjCn8Qv9cpyJFyogTYqkP5WCobX5RNaHsJzIV/6NvAnlnouOcwKr29QrxLGDGcqYuJ+ae98s1A== - -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-string-escape@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" - integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8= +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -js-yaml@^3.13.1, js-yaml@^3.14.0: +js-yaml@^3.13.1: version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" @@ -3611,42 +3410,40 @@ js-yaml@^3.13.1, js-yaml@^3.14.0: js-yaml@^3.14.1: version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: 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.npmjs.org/json-colorizer/-/json-colorizer-2.2.2.tgz" + 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.npmjs.org/json-cycle/-/json-cycle-1.5.0.tgz" + integrity sha512-GOehvd5PO2FeZ5T4c+RxobeT5a1PiGpF4u9/3+UvrMU4bhnVqzJY7hm39wg8PDCqkU91fWGH8qjWR4bn+wgq9w== json-refs@^3.0.15: version "3.0.15" - resolved "https://registry.yarnpkg.com/json-refs/-/json-refs-3.0.15.tgz#1089f4acf263a3152c790479485195cd6449e855" + resolved "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz" integrity sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw== dependencies: commander "~4.1.1" @@ -3658,450 +3455,207 @@ 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.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + 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" -jsonpath-plus@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-3.0.0.tgz#194ab4792a5e9b4ed27bf442188c8eb7e697a04b" - integrity sha512-WQwgWEBgn+SJU1tlDa/GiY5/ngRpa9yrSj8n4BYPHcwoxTDaMEaYCHMOn42hIHHDd3CrUoRr3+HpsK0hCKoxzA== - -jsonschema@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.3.0.tgz#def86ca039b4f4254e76528d173462bc5da36162" - integrity sha512-qg48ckmeeQNPyPAUVIb4Qgmg/U2Kgg5SuEyMs8Z72cnxsw5Ra088U/Foi6sMp/cs7sZ+LNrmvX0Ww+ohE2By0g== - -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -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.1.0, jszip@^3.2.2, 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, jszip@^3.7.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" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.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= - -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" + resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz" + integrity sha512-86GgN2vzfUu7m9Wcj63iUkuDzFNYFVmjeDm2GzWpUk+opB0pEpMsw6ePCMrhYkumz2C1ihqtZzOMAg7FiXcNoQ== -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.2" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz" + integrity sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g== 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" - -latest-version@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -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" lie@~3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.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" + resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - 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.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== 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.uniqby@^4.0.0: +lodash.uniqby@^4.7.0: version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" + resolved "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= lodash.values@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + resolved "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz" integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= -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-timeout@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" - integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= - -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" -luxon@^1.22.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" - integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== - 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" - -make-dir@^3.0.0: +make-dir@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" -map-age-cleaner@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.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= - dependencies: - object-visit "^1.0.0" - -md5-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-4.0.0.tgz#f3f7ba1e2dd1144d5bf1de698d0e5f44a4409584" - integrity sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg== - -mem@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/mem/-/mem-6.1.1.tgz#ea110c2ebc079eca3022e6b08c85a795e77f6318" - integrity sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q== - dependencies: - map-age-cleaner "^0.1.3" - mimic-fn "^3.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== - 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" - -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" @@ -4113,564 +3667,341 @@ memoizee@^0.4.15: next-tick "^1.1.0" timers-ext "^0.1.7" -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -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: version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -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" + +micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" -mime-db@1.44.0: +mime-db@1.44.0, mime-db@^1.28.0: version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-db@1.x.x, 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-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12: version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: mime-db "1.44.0" -mime@^1.2.11, mime@^1.4.1: +mime@^1.4.1: version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -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-fn@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" - integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== - -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.0.4, minimatch@^3.1.1, minimatch@^5.0.1, minimatch@^6.1.6, minimatch@~3.0.4: + version "6.2.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-6.2.0.tgz#2b70fd13294178c69c04dfc05aebdb97a4e79e42" + integrity sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" -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== +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.npmjs.org/minipass/-/minipass-3.3.6.tgz" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + minizlib@^2.1.1: version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== dependencies: 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, mkdirp@^0.5.5: - 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" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment-timezone@^0.5.31: - version "0.5.31" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" - integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0", 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: 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" + resolved "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz" integrity sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE= -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" + resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" 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= + resolved "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz" + 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-schedule@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb" - integrity sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw== +node-fetch@^2.6.11, node-fetch@^2.6.7, node-fetch@^2.6.8: + version "2.6.12" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz" + integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== dependencies: - cron-parser "^2.7.3" - long-timeout "0.1.1" - sorted-array-functions "^1.0.0" + 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== - -npm-run-path@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" +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.npmjs.org/npm-registry-utilities/-/npm-registry-utilities-1.0.0.tgz" + 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= - -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@^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.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-keys@^1.0.12, 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-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= - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object.fromentries@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" - integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" +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== 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.npmjs.org/open/-/open-7.4.2.tgz" + 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" - integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + version "2.1.1" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== -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@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" -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== - dependencies: - yocto-queue "^0.1.0" - p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" -p-memoize@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/p-memoize/-/p-memoize-4.0.1.tgz#6f4231857fec10de2504611fe820c808fa8c5f8b" - integrity sha512-km0sP12uE0dOZ5qP+s7kGVf07QngxyG0gS8sYFvFWhqlgzOsSy+m71aUejf/0akxj5W7gE//2G74qTv6b4iMog== - dependencies: - mem "^6.0.1" - mimic-fn "^3.0.0" - -p-queue@^6.3.0: - version "6.6.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" - integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== - dependencies: - eventemitter3 "^4.0.4" - p-timeout "^3.2.0" - -p-retry@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.2.0.tgz#ea9066c6b44f23cab4cd42f6147cdbbc6604da5d" - integrity sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA== - dependencies: - "@types/retry" "^0.12.0" - retry "^0.12.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== - dependencies: - p-finally "^1.0.0" - -p-timeout@^3.2.0: +p-timeout@^3.1.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== dependencies: p-finally "^1.0.0" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -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== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - pako@~1.0.2: version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" 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@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 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= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== path-loader@^1.0.10: version "1.0.10" - resolved "https://registry.yarnpkg.com/path-loader/-/path-loader-1.0.10.tgz#dd3d1bd54cb6f2e6423af2ad334a41cc0bce4cf6" + resolved "https://registry.npmjs.org/path-loader/-/path-loader-1.0.10.tgz" integrity sha512-CMP0v6S6z8PHeJ6NFVyVJm6WyJjIwFvyz2b0n2/4bKdS/0uZa/9sKUlYZzubrn3zuDRU0zIuEDX9DZYQ2ZI8TA== dependencies: native-promise-only "^0.8.1" @@ -4678,236 +4009,94 @@ path-loader@^1.0.10: 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.npmjs.org/path2/-/path2-0.1.0.tgz" + integrity sha1-Y5golCzb2kSkGkWwdK6Ic0g7Tvo= + +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== -please-upgrade-node@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" - integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== - dependencies: - semver-compare "^1.0.0" - -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= - -portfinder@^1.0.25: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" - -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== - dependencies: - colors "1.3.x" - commander "2.19.x" - lodash "4.17.x" - printj@~1.1.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + resolved "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz" integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== 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" + resolved "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz" 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== - -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" @@ -4915,92 +4104,44 @@ 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= + resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== -punycode@^2.1.0, punycode@^2.1.1: +punycode@^2.1.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pupa@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" - integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== +qs@^6.11.0, qs@^6.5.1: + version "6.12.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" + integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== dependencies: - escape-goat "^2.0.0" - -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== + side-channel "^1.0.6" -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== +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== -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== - dependencies: - decode-uri-component "^0.2.0" - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" +querystring@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== -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= +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + 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" @@ -5011,832 +4152,513 @@ 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@^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, 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" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" +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: + readable-stream "^3.6.0" + readdir-glob@^1.0.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" + resolved "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz" integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== dependencies: minimatch "^3.0.4" -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== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" -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== - dependencies: - component-emitter "^1.2.1" - debug "^2.6.8" - readline-utils "^2.2.1" - string-width "^2.0.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= - 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" - -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== - 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= - -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-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== require-main-filename@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== resolve-alpn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c" - 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" + version "1.2.1" + resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== responselike@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" - integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + version "2.0.1" + resolved "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== 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= - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - 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== -rimraf@^2.6.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -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.npmjs.org/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz" + 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-buffer@5.1.2, 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" - 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: +safe-buffer@5.2.1, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"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== + resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" + 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" - integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== - dependencies: - commander "^2.8.1" - -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= - -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -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@^6.0.0, semver@^6.1.1, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.1.3: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - -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" - -serverless-domain-manager@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/serverless-domain-manager/-/serverless-domain-manager-5.1.0.tgz#44c975b4adeac6b32507c92313ee001735d56c55" - integrity sha512-uGLGr9nWTupimWxVwz/2S/fK+YULsg8hQ0ZddS1DNerzctIVQBXVbLMGUJv34a9a5HV0YPvVqCA/JBNI7VOSvA== + resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== dependencies: - aws-sdk "^2.756.0" - chalk "^4.1.0" + commander "^2.8.1" -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== +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +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.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: + version "7.6.2" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +serverless-domain-manager@^7.0.4: + version "7.0.4" + resolved "https://registry.npmjs.org/serverless-domain-manager/-/serverless-domain-manager-7.0.4.tgz" + integrity sha512-6ARC3XP60z2bJ8hRCwKkLqpeozn94gTdqgwjHVP75rFqdNVF4mu57EUyV3MOKrqd53D2lbJdf6PS3xm/8raYRw== + dependencies: + "@aws-sdk/client-acm" "^3.329.0" + "@aws-sdk/client-api-gateway" "^3.329.0" + "@aws-sdk/client-apigatewayv2" "^3.329.0" + "@aws-sdk/client-cloudformation" "^3.329.0" + "@aws-sdk/client-route-53" "^3.329.0" + "@aws-sdk/client-s3" "^3.329.0" + "@aws-sdk/config-resolver" "^3.329.0" + "@aws-sdk/credential-providers" "^3.329.0" + "@aws-sdk/node-config-provider" "^3.329.0" + "@aws-sdk/smithy-client" "^3.329.0" + +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" - fs-copy-file "^2.1.2" - mkdirp "^0.5.1" - -serverless-offline@^6.1.5: - version "6.8.0" - resolved "https://registry.yarnpkg.com/serverless-offline/-/serverless-offline-6.8.0.tgz#65d2192e41ef87ebcbc1093108db6a25059d6ab5" - integrity sha512-DBDMcU58Bl+zZGSTAZ96Ed57k11oh0fQwgSoH2iVJpO6xFV9dTIttBYzTCwnfAgG0kB6NZK99Q/69b4brChTnQ== - dependencies: - "@hapi/boom" "^7.4.11" - "@hapi/h2o2" "^8.3.2" - "@hapi/hapi" "^18.4.1" - aws-sdk "^2.624.0" - boxen "^4.2.0" chalk "^3.0.0" - cuid "^2.1.8" - execa "^4.0.0" - extend "^3.0.2" + folder-hash "^3.3.0" + fs-copy-file "^2.1.2" fs-extra "^8.1.0" - java-invoke-local "0.0.6" - js-string-escape "^1.0.1" - jsonpath-plus "^3.0.0" - jsonschema "^1.2.6" - jsonwebtoken "^8.5.1" - jszip "^3.2.2" - luxon "^1.22.0" - node-fetch "^2.6.0" - node-schedule "^1.3.2" - object.fromentries "^2.0.2" - p-memoize "^4.0.0" - p-queue "^6.3.0" - p-retry "^4.2.0" - please-upgrade-node "^3.2.0" - portfinder "^1.0.25" - semver "^7.1.3" - update-notifier "^4.1.0" - velocityjs "^2.0.0" - ws "^7.2.1" + 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.2: - 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" - -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== + bluebird "^3.7.2" -serverless-python-requirements@^4.2.5: - version "4.3.0" - resolved "https://registry.yarnpkg.com/serverless-python-requirements/-/serverless-python-requirements-4.3.0.tgz#ba2b78e01213428ecd62a6368af82beec4de17d1" - integrity sha512-VyXdEKNxUWoQDbssWZeR5YMaTDf1U4CO3yJH2953Y2Rt8zD6hG+vpTkVR490/Ws1PQsBopWuFfgDcLyvAppaRg== +serverless-python-requirements@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/serverless-python-requirements/-/serverless-python-requirements-6.0.0.tgz" + integrity sha512-sjPgsqDpA4nNaa+kCbY7R+RSf58F6iWmmfv4qaJ+EvN0nIr/2KKfI5fudyHf63EPF3VnKv3uUdj23V0oT3uYZA== dependencies: + "@iarna/toml" "^2.2.5" appdirectory "^0.1.0" - bluebird "^3.0.6" - fs-extra "^7.0.0" - glob-all "^3.1.0" - is-wsl "^1.1.0" - jszip "^3.1.0" + bluebird "^3.7.2" + child-process-ext "^2.1.1" + fs-extra "^10.1.0" + glob-all "^3.3.0" + is-wsl "^2.2.0" + jszip "^3.10.1" lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniqby "^4.0.0" + lodash.uniqby "^4.7.0" lodash.values "^4.3.0" - md5-file "^4.0.0" - rimraf "^2.6.2" - shell-quote "^1.6.1" + rimraf "^3.0.2" + set-value "^4.1.0" + sha256-file "1.0.0" + shell-quote "^1.7.4" -serverless-wsgi@^1.5.2: - version "1.7.5" - resolved "https://registry.yarnpkg.com/serverless-wsgi/-/serverless-wsgi-1.7.5.tgz#7598437bd574e1999c4adee92e877ab99cb97637" - integrity sha512-6a+rHqAemAJuZiH9ecqwoigiVZKuATIxJft0CUWAxXwietzg0BkSowcUKcfah+fbjgSQks7LDl8B+xe8Kb7waA== +serverless-wsgi@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/serverless-wsgi/-/serverless-wsgi-3.0.2.tgz" + integrity sha512-9cu+WPxS2+YNpptFOCCp5Wq2+M6Dfjg7QV/SyLcW20OM7DE5zprI4SduMU0Dq3fXRVXFUdH4E0ll57s/FqIPeQ== dependencies: bluebird "^3.7.2" - fs-extra "^9.0.0" + fs-extra "^10.1.0" hasbin "^1.2.3" - lodash "^4.17.15" - -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" + lodash "^4.17.21" + process-utils "^4.0.0" + +serverless@^3.32.2: + version "3.33.0" + resolved "https://registry.npmjs.org/serverless/-/serverless-3.33.0.tgz" + integrity sha512-qmG0RMelsWmnS5Smxoy0CbjpecgnJlM89wzSIgJqfkGlmOo2nJdd8y0/E6KlaTsaozlPKkjUBDzis2nF8VNO2g== + dependencies: + "@serverless/dashboard-plugin" "^6.2.3" + "@serverless/platform-client" "^4.3.2" + "@serverless/utils" "^6.11.1" + 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" + got "^11.8.6" + 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, set-blocking@~2.0.0: +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= + resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -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-value@^4.0.1, set-value@^4.1.0: + 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-plain-object "^2.0.4" + is-primitive "^3.0.1" -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== - dependencies: - is-extendable "^0.1.1" - kind-of "^5.0.0" - mixin-object "^2.0.1" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -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" +sha256-file@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/sha256-file/-/sha256-file-1.0.0.tgz" + integrity sha512-nqf+g0veqgQAkDx0U2y2Tn2KWyADuuludZTw9A7J3D+61rKlIIl9V5TS4mfnwKuXZOH9B7fQyjYJ9pKRHIsAyg== 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-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.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= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== -shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +shell-quote@^1.7.3, shell-quote@^1.7.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -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== +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: - nanoid "^2.1.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== + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" -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== - -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== - dependencies: - decompress-response "^3.3.0" - once "^1.3.1" - simple-concat "^1.0.0" +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.16.1" + resolved "https://registry.npmjs.org/simple-git/-/simple-git-3.16.1.tgz" + integrity sha512-xzRxMKiy1zEYeHGXgAzvuXffDS0xgsq07Oi4LWEEcVH29vLpcZ2tyQRWyK0NLLlCVaKysZeem5tC1qHEOxsKwA== 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.4" slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" 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" - -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" -sorted-array-functions@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5" - integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== - -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: version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + resolved "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz" integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== dependencies: readable-stream "^3.0.0" sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" 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" - -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.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== stream-promise@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/stream-promise/-/stream-promise-3.2.0.tgz#bad976f2d0e1f11d56cc95cc11907cfd869a27ff" + resolved "https://registry.npmjs.org/stream-promise/-/stream-promise-3.2.0.tgz" integrity sha512-P+7muTGs2C8yRcgJw/PPt61q7O517tDHiwYEzMWo1GSBCcZedUMT/clz7vUNsSxFphIlJ6QUL4GexQKlfJoVtA== dependencies: "2-thenable" "^1.0.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= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"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== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.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== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.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-width@^4.1.0, string-width@^4.2.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: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + strip-ansi "^6.0.1" 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-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -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= +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz" + 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@^3.8.3: version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + resolved "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz" integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== dependencies: component-emitter "^1.2.0" @@ -5852,43 +4674,50 @@ superagent@^3.8.3: supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 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" @@ -5899,10 +4728,10 @@ 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: - version "2.1.4" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" - integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== +tar-stream@^2.1.0, tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== dependencies: bl "^4.0.3" end-of-stream "^1.4.1" @@ -5910,60 +4739,31 @@ 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@^6.1.15: + version "6.2.1" + resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" + 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" -term-size@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" - integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== - -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" + resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== 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" @@ -5971,325 +4771,208 @@ 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.7" + resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz" + integrity sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg== 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" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" 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= - -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== - -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== +tslib@^2.1.0, tslib@^2.3.1, tslib@^2.5.0: + version "2.6.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz" + integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +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: - version "2.1.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" - 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== - dependencies: - is-typedarray "^1.0.0" +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== 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" -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== +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: - crypto-random-string "^2.0.0" + 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" + resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -update-notifier@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" - integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== - dependencies: - boxen "^4.2.0" - chalk "^3.0.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.3.1" - is-npm "^4.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.0.0" - pupa "^2.0.1" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - uri-js@^4.2.2: version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz" integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== 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= + resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz" + 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.npmjs.org/util/-/util-0.12.5.tgz" + 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.npmjs.org/uuid/-/uuid-8.0.0.tgz" + 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== -velocityjs@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/velocityjs/-/velocityjs-2.0.2.tgz#834f488e0ee4a9f63ff4201bf1e524ac51bdcfbf" - integrity sha512-TUQ7/lOEFEho7zSXlh6M/+lAOIRU0g7nMDUlGn1Jt40Y0JLOnIVM4RTuB4KpkN6eL7BPl+ygc2zi5XJIi874zQ== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz" + integrity sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw== dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" + builtins "^1.0.3" -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= +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: + defaults "^1.0.3" + +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== + +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: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" which-module@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" + integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== -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= +which-typed-array@^1.1.2: + version "1.1.9" + resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" which@^1.2.9: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -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== - dependencies: - string-width "^1.0.2 || 2" - -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" - -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== - dependencies: - define-property "^1.0.0" - is-number "^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== - dependencies: - readable-stream "^2.3.7" - triple-beam "^1.2.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== - 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" - wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -6298,7 +4981,7 @@ wrap-ansi@^6.2.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" @@ -6307,98 +4990,81 @@ 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.0, 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" + signal-exit "^3.0.7" -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" +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@^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@~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" - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + 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== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.3" + resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +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" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml-ast-parser@0.0.43: version "0.0.43" - resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" + resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== yamljs@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b" + resolved "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz" integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ== dependencies: argparse "^1.0.7" @@ -6406,20 +5072,15 @@ yamljs@^0.3.0: yargs-parser@^18.1.2: version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" -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== - yargs@^15.3.1: version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" @@ -6434,27 +5095,36 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yarn-audit-fix@^10.0.0: + version "10.0.7" + resolved "https://registry.npmjs.org/yarn-audit-fix/-/yarn-audit-fix-10.0.7.tgz" + integrity sha512-JC6Uu/GAY/cG5k4GZDZk2MgmygiN+FY/mSM1fKY2w6myBg/qVdI/jDeOCsbsuHXf0TsMpd2LcF8yGwqvQ+X4Kw== + dependencies: + "@types/fs-extra" "^11.0.1" + "@types/lodash-es" "^4.17.8" + "@types/semver" "^7.5.0" + "@types/yarnpkg__lockfile" "^1.1.6" + "@yarnpkg/lockfile" "^1.1.0" + chalk "^5.3.0" + commander "^11.0.0" + fast-glob "^3.3.1" + fs-extra "^11.1.1" + js-yaml "^4.1.0" + lodash-es "^4.17.21" + semver "^7.5.4" + synp "^1.9.10" + 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== - 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" @@ -6462,27 +5132,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.0" + resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz" + integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== dependencies: archiver-utils "^2.1.0" - compress-commons "^4.0.2" + compress-commons "^4.1.0" readable-stream "^3.6.0" diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/.gitignore b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/.gitignore deleted file mode 100644 index 1521c8b76..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/index.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/index.js deleted file mode 100644 index f47370da2..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/index.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; -const AWS = require('aws-sdk'); -const invalidateDistributions = require('./src/invalidate-distributions'); -const getDomain = require('./src/get-domain'); - -class InvalidateCloudfront { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options || {}; - - this.commands = { - invalidate: { - usage: 'Invalidate the Cloudfront cache', - lifecycleEvents: ['invalidate'] - }, - 'get-domain': { - usage: 'Get a cloudfront domain', - lifecycleEvents: ['get-domain'], - options: { - distribution: { - usage: 'Specify the distribution you want to the domain of (e.g. "--distribution MyDistribution")', - shortcut: 'd', - required: true - } - } - } - }; - - this.hooks = { - 'invalidate:invalidate': () => this.invalidateCloudfrontDistributions(), - 'get-domain:get-domain': () => this.getDomain() - }; - } - - invalidateCloudfrontDistributions() { - const distributions = this.serverless.service.custom.invalidateCloudfront; - const resources = this.serverless.service.resources.Resources; - const cli = this.serverless.cli; - const aws = this.serverless.getProvider('aws'); - - if (distributions === undefined || resources === undefined) { - return; - } - - const credentials = aws.getCredentials().credentials; - const cloudfront = new AWS.CloudFront({ - credentials - }); - - return invalidateDistributions(aws, distributions, cloudfront, cli); - } - - getDomain() { - const aws = this.serverless.getProvider('aws'); - - const credentials = aws.getCredentials().credentials; - const cloudfront = new AWS.CloudFront({ - credentials - }); - const dist = this.options.distribution; - - return getDomain(dist, aws, cloudfront).then((domain) => { - console.log(domain); - }); - } -} - -module.exports = InvalidateCloudfront; diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/jasmine.json b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/jasmine.json deleted file mode 100644 index 922f84aa7..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/jasmine.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "spec_dir": "./src", - "spec_files": ["**/*[sS]pec.js"], - "stopSpecOnExpectationFailure": false -} diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/package.json b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/package.json deleted file mode 100644 index 7fbc163d0..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "serverless-invalidate-cloudfront", - "version": "1.0.0", - "description": "Serverless plugin that automatically performs cloudfront invalidation on deploy.", - "main": "index.js", - "license": "UNLICENSED", - "scripts": { - "pretest": "yarn install", - "test": "./node_modules/.bin/jasmine --config=jasmine.json" - }, - "dependencies": { - "aws-sdk": "^2.214.1" - }, - "devDependencies": { - "jasmine": "^3.1.0" - } -} diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/get-domain.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/get-domain.js deleted file mode 100644 index c99d01868..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/get-domain.js +++ /dev/null @@ -1,37 +0,0 @@ -function getNameFromDistributionInfo(distributionId, cloudfront) { - const params = { - Id: distributionId - }; - - return cloudfront - .getDistribution(params, (data, err) => {}) - .promise() - .then((data) => { - if (!data.DomainName) { - throw Error('GetDomain: No domain name found'); - } - return data.DomainName; - }); -} - -/** - * Retrieves the domain name from a distribution. - * @param {string} distributionName The resource name of the distribution - * @param {AWS} aws The aws sdk - * @param {Cloudfront} cloudfront An AWS cloudfront service. - * @returns {Promise} A promise containing the domain name of the resource. - */ -function getDomain(distributionName, aws, cloudfront) { - const stackName = aws.naming.getStackName(); - - return aws.request('CloudFormation', 'describeStackResources', { StackName: stackName }).then((resp) => { - const stackResources = resp.StackResources; - const resource = stackResources.find((r) => r.LogicalResourceId === distributionName); - if (resource === undefined) { - throw Error(`Unable to find distribution matching '${name}'.`); - } - return getNameFromDistributionInfo(resource.PhysicalResourceId, cloudfront); - }); -} - -module.exports = getDomain; diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/get-domain.spec.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/get-domain.spec.js deleted file mode 100644 index 711d34564..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/get-domain.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -const getDomain = require('./get-domain'); - -describe('getDomain', () => { - let aws, cloudfront, promise; - beforeEach(() => { - const stacks = { - StackResources: [ - { - ResourceType: 'AWS::CloudFront::Distribution', - LogicalResourceId: 'dist1', - PhysicalResourceId: 'physical1' - } - ] - }; - const stackName = 'stack'; - aws = { - naming: jasmine.createSpyObj('naming', { - getStackName: jasmine.createSpy('getStackName').and.returnValue(stackName) - }), - request: jasmine.createSpy('request').and.returnValue(Promise.resolve(stacks)) - }; - - cloudfront = { - getDistribution: jasmine.createSpy('getDistribution').and.returnValue({ - promise: () => - Promise.resolve({ - DomainName: 'www.somedomain.com' - }) - }) - }; - }); - - it('gets a domain name from a distribution name', (done) => { - getDomain('dist1', aws, cloudfront).then((name) => { - expect(name).toBe('www.somedomain.com'); - done(); - }); - }); -}); diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/invalidate-distributions.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/invalidate-distributions.js deleted file mode 100644 index 2f8d503b3..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/invalidate-distributions.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; -const randomString = require('./random-string'); -const matchDistributions = require('./match-distributions'); - -/** - * A distribution configuration object. - * @typedef {Object} Distribution - * @property {String[]} paths - An array of paths to invalidate. - */ - -/** - * A distribution/physical id pair. - * @typedef {Object} DistributionInfo - * @property {Distribution} distribution - * @property {String} distributionId - * @property {String} name - */ - -/** - * Invalidates a cloudfront distribution. - * @param {DistributionInfo} distributionInfo - Info about the distribution to invalidate. - * @param {Cloudfront} cloudfront - An AWS cloudfront service. - * @param {Cli} cli - An serverless cli service. - */ -function invalidateDistribution(distributionInfo, cloudfront, cli) { - const reference = randomString(16); - const paths = distributionInfo.distribution.paths; - const distributionId = distributionInfo.distributionId; - const name = distributionInfo.name; - - if (paths === undefined) { - return Promise.reject('No paths defined'); - } - - const params = { - DistributionId: distributionId, - InvalidationBatch: { - CallerReference: reference, - Paths: { - Quantity: paths.length, - Items: paths - } - } - }; - - return cloudfront - .createInvalidation(params, (error, data) => { - if (!error) { - cli.log(`InvalidateCloudfront: Invalidating Distribution '${name}(${distributionInfo.distributionId})'`); - } - }) - .promise(); -} - -/** - * Retrieves information about a stack from AWS, and invalidates all matching - * cloudfront distributions. - * @param {Aws} aws - The AWS service. - * @param {Array} distributions - A list of distribution objects, including invalidation paths. - * @param {Cloudfront} cloudfront - An AWS cloudfront service. - * @param {Cli} cli - An serverless cli service. - */ -function invalidateDistributions(aws, distributions, cloudfront, cli) { - const stackName = aws.naming.getStackName(); - - return aws - .request('CloudFormation', 'describeStackResources', { StackName: stackName }) - .then((resp) => { - return matchDistributions(distributions, resp.StackResources); - }) - .then((distributions) => { - const promises = distributions.map((pair) => invalidateDistribution(pair, cloudfront, cli)); - return Promise.all(promises); - }); -} - -module.exports = invalidateDistributions; diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/invalidate-distributions.spec.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/invalidate-distributions.spec.js deleted file mode 100644 index 1e78fd34b..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/invalidate-distributions.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -const invalidateDistributions = require('./invalidate-distributions'); - -describe('invalidateDistributions', () => { - let aws, cli, cloudfront, promise, distributions; - beforeEach(() => { - const stacks = { - StackResources: [ - { - ResourceType: 'AWS::CloudFront::Distribution', - LogicalResourceId: 'dist1', - PhysicalResourceId: 'physical1' - }, - { - ResourceType: 'AWS::CloudFront::Distribution', - LogicalResourceId: 'dist2', - PhysicalResourceId: 'physical2' - }, - { - ResourceType: 'AWS::Lambda', - LogicalResourceId: 'nondistribution', - PhysicalResourceId: 'physical3' - } - ] - }; - const stackName = 'stack'; - aws = { - naming: jasmine.createSpyObj('naming', { - getStackName: jasmine.createSpy('getStackName').and.returnValue(stackName) - }), - request: jasmine.createSpy('request').and.returnValue(Promise.resolve(stacks)) - }; - - cloudfront = { - createInvalidation: jasmine.createSpy('createInvalidation').and.returnValue({ promise: () => Promise.resolve() }) - }; - - cli = jasmine.createSpyObj('cli', { - log: jasmine.createSpy('log') - }); - distributions = { - dist1: { - paths: ['a', 'b', 'c'] - } - }; - promise = invalidateDistributions(aws, distributions, cloudfront, cli); - }); - it('calls createInvalidation with the correct distribution id', (done) => { - promise.then(() => { - expect(cloudfront.createInvalidation).toHaveBeenCalledWith( - jasmine.objectContaining({ - DistributionId: 'physical1' - }), - jasmine.any(Function) - ); - done(); - }); - }); - it('calls createInvalidation with the correct paths', (done) => { - promise.then(() => { - expect(cloudfront.createInvalidation).toHaveBeenCalledWith( - jasmine.objectContaining({ - InvalidationBatch: jasmine.objectContaining({ - Paths: { - Quantity: 3, - Items: distributions.dist1.paths - } - }) - }), - jasmine.any(Function) - ); - done(); - }); - }); - it('rejects distributions without paths', (done) => { - distributions = { - dist1: {} - }; - promise = invalidateDistributions(aws, distributions, cloudfront, cli); - promise.catch(() => { - done(); - }); - }); -}); diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/match-distributions.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/match-distributions.js deleted file mode 100644 index d54d0674b..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/match-distributions.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -/** - * A distribution configuration object. - * @typedef {Object} Distribution - * @property {String[]} paths - An array of paths to invalidate. - */ - -/** - * A stack resource object. - * @typedef {Object} StackResource - * @property {String} LogicalResourceId - * @property {String} PhysicalResourceId - * @property {String} ResourceType - */ - -/** - * A distribution/physical id pair. - * @typedef {Object} DistributionInfo - * @property {Distribution} distribution - * @property {String} distributionId - * @property {String} name - */ - -/** - * - * @param {Object.} distributions - Array of distribution configuration options - * @param {Array} stackResources - * @param {String} stackName - The name of the stack. - * @returns {DistributionInfo[]} - A distribution paired with it's physical id. - */ -function matchDistributions(distributions, stackResources, stackName) { - const CLOUDFRONT_TYPE = 'AWS::CloudFront::Distribution'; - - if (distributions === undefined) { - return []; - } - - return Object.keys(distributions) - .map((distributionName) => { - const distribution = distributions[distributionName]; - - const resource = stackResources.find((r) => r.LogicalResourceId === distributionName); - - if (!resource) { - throw new Error( - `InvalidateCloudfront: Stack '${stackName}'did not have a resource with logical name '${distributionName}'` - ); - } - if (resource.ResourceType !== CLOUDFRONT_TYPE) { - throw new Error( - `InvalidateCloudfront: Stack '${stackName}' had resource with logical name '${distributionName}', but was of incorrect type '${resource.ResourceType}'` - ); - } - const distributionId = resource.PhysicalResourceId; - return { distributionId, distribution, name: distributionName }; - }) - .filter((pair) => pair !== undefined); -} - -module.exports = matchDistributions; diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/match-distributions.spec.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/match-distributions.spec.js deleted file mode 100644 index fed03ea4c..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/match-distributions.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -const matchDistributions = require('./match-distributions'); - -describe('matchDistributions', () => { - let stacks; - - beforeEach(() => { - stacks = [ - { - ResourceType: 'AWS::CloudFront::Distribution', - LogicalResourceId: 'dist1', - PhysicalResourceId: 'physical1' - }, - { - ResourceType: 'AWS::CloudFront::Distribution', - LogicalResourceId: 'dist2', - PhysicalResourceId: 'physical2' - }, - { - ResourceType: 'AWS::Lambda', - LogicalResourceId: 'nondistribution', - PhysicalResourceId: 'physical3' - } - ]; - }); - - it('can match a single distribution to a stack resource', () => { - const distributions = { - dist1: { - paths: ['a', 'b', 'c'] - } - }; - const result = matchDistributions(distributions, stacks, 'staging'); - expect(result).toEqual([{ distribution: distributions.dist1, distributionId: 'physical1', name: 'dist1' }]); - }); - it('can match multiple distributions to a stack resource', () => { - const distributions = { - dist1: { - paths: ['a', 'b', 'c'] - }, - dist2: { - paths: ['e', 'f', 'g'] - } - }; - const result = matchDistributions(distributions, stacks, 'staging'); - expect(result).toEqual([ - { distribution: distributions.dist1, distributionId: 'physical1', name: 'dist1' }, - { distribution: distributions.dist2, distributionId: 'physical2', name: 'dist2' } - ]); - }); - - it("throws an error when a distribution doesn't have a matching stack resource", () => { - const distributions = { - dist3: { - paths: ['a', 'b', 'c'] - } - }; - expect(() => matchDistributions(distributions, stacks, 'staging')).toThrow(); - }); - - it("throws an error when a distribution matches a stack resource which isn't a cloudfront distribution", () => { - const distributions = { - nondistribution: { - paths: ['a', 'b', 'c'] - } - }; - expect(() => matchDistributions(distributions, stacks, 'staging')).toThrow(); - }); -}); diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/random-string.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/random-string.js deleted file mode 100644 index ba9699153..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/random-string.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const randomCharacter = () => { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const index = Math.floor(Math.random() * chars.length) % chars.length; - return chars.charAt(index); -}; - -/** - * Generates a random alphanumeric ASCII string of the given length. - * @param {number} length - */ -function randomString(length) { - let text = ''; - for (let i = 0; i < length; ++i) { - text += randomCharacter(); - } - return text; -} - -module.exports = randomString; diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/random-string.spec.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/random-string.spec.js deleted file mode 100644 index 61f4f669d..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-invalidate-cloudfront/src/random-string.spec.js +++ /dev/null @@ -1,16 +0,0 @@ -const randomString = require('./random-string'); - -describe('random-string', () => { - let stacks; - - it('will generate a string of the correct length', () => { - const result = randomString(16); - expect(result.length).toBe(16); - }); - - it("won't generate the same string twice", () => { - const result1 = randomString(16); - const result2 = randomString(16); - expect(result1).not.toBe(result2); - }); -}); diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.editorconfig b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.editorconfig deleted file mode 100644 index fa21f7792..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.editorconfig +++ /dev/null @@ -1,10 +0,0 @@ -root = true - -[*] -indent_style = space -end_of_line = lf - -[*.{js,jsx}] -indent_style = space -end_of_line = lf -charset = utf-8 diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.eslintrc b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.eslintrc deleted file mode 100644 index 2a8871517..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.eslintrc +++ /dev/null @@ -1,50 +0,0 @@ ---- -# Based on the AirBnb JavaScript styleguides with our own twist -env: - es6: true - node: true - mocha: true -extends: - - eslint:recommended - - plugin:lodash/recommended - - plugin:promise/recommended - - plugin:import/errors - - plugin:import/warnings -parser: babel-eslint -parserOptions: - sourceType: module - ecmaFeatures: - classes: true - experimentalObjectRestSpread: true -plugins: - - promise - - lodash - - import -rules: - indent: [ error, 2, { - MemberExpression: off - } ] - linebreak-style: [ error, unix ] - semi: [ error, always ] - quotes: [ error, single, { avoidEscape: true } ] - no-mixed-spaces-and-tabs: error - space-before-blocks: error - arrow-spacing: error - key-spacing: [ error, { afterColon: true, mode: minimum } ] - brace-style: [ error, '1tbs' ] - comma-spacing: [ error, { before: false, after: true } ] - comma-style: [ error, last, { exceptions: { VariableDeclaration: true } } ] - array-bracket-spacing: [ error, always, { singleValue: false } ] - computed-property-spacing: [ error, never ] - object-curly-spacing: [ error, always ] - prefer-const: error - no-var: error - promise/no-nesting: off - import/first: error - import/newline-after-import: error - import/no-named-as-default: off - import/no-extraneous-dependencies: [ error, { devDependencies: true } ] - lodash/import-scope: off - lodash/preferred-alias: off - lodash/prop-shorthand: off - lodash/prefer-lodash-method: [ error, { ignoreObjects: [ BbPromise, path ] } ] diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.gitignore b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.gitignore deleted file mode 100644 index e9cc9995d..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -node_modules -dist -.webpack -.serverless -coverage - -.idea -/.nyc_output -node_modules -dist -.webpack -coverage - diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.npmignore b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.npmignore deleted file mode 100644 index 1ca47212f..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -coverage -examples -tests diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.travis.yml b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.travis.yml deleted file mode 100644 index efc8420b9..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js - -matrix: - include: - - node_js: '6.10.1' - -sudo: false - -install: - - travis_retry npm install - -script: - - npm run eslint - # - npm test - -# after_success: -# - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/LICENSE b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/LICENSE deleted file mode 100644 index 63b4b681c..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) [year] [fullname] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/index.js b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/index.js deleted file mode 100644 index 039406bc2..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/index.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -const _ = require('lodash'); - -class LambdaArn { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options; - this.hooks = { - 'before:package:finalize': this.updateLambdaVersion.bind(this) - }; - } - - updateLambdaVersion() { - const resources = this.serverless.service.resources.Resources; - const compiledResources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources; - const lambdaArns = this.getResourcesWLambdaAssoc(resources); - - _.forEach(lambdaArns, (value) => { - const associations = value.Properties.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations; - - _.forEach(associations, (association) => { - const arn = association.LambdaFunctionARN; - const versionRef = this.getArnAndVersion(compiledResources, arn); - if (arn && versionRef) { - this.serverless.cli.log(`serverless-lambda-version: injecting arn+version for ${JSON.stringify(arn)}`); - association.LambdaFunctionARN = versionRef; - } - }); - }); - } - - getArnAndVersion(resources, funcNormName) { - const key = _.findKey(resources, { - Type: 'AWS::Lambda::Version', - Properties: { - FunctionName: { - Ref: funcNormName - } - } - }); - - return key - ? { - 'Fn::Join': ['', [{ 'Fn::GetAtt': [funcNormName, 'Arn'] }, ':', { 'Fn::GetAtt': [key, 'Version'] }]] - } - : undefined; - } - - getResourcesWLambdaAssoc(resources) { - const eventTypes = ['viewer-request', 'origin-request', 'origin-response', 'viewer-response']; - return eventTypes - .map((eventType) => this.getResourcesWLambdaAssocOfType(resources, eventType)) - .reduce((previous, current) => ({ ...previous, ...current }), {}); - } - - getResourcesWLambdaAssocOfType(resources, eventType) { - return _.pickBy(resources, { - Type: 'AWS::CloudFront::Distribution', - Properties: { - DistributionConfig: { - DefaultCacheBehavior: { - LambdaFunctionAssociations: [ - { - EventType: eventType - } - ] - } - } - } - }); - } -} - -module.exports = LambdaArn; diff --git a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/package.json b/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/package.json deleted file mode 100644 index 8b04f4146..000000000 --- a/cla-frontend-contributor-console/.serverless_plugins/serverless-lambda-version/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "serverless-lambda-version", - "version": "0.1.2", - "description": "Serverless plugin to auto update lambda function version for Lambda@Edge LambdaFunctionAssociations", - "main": "index.js", - "author": "Dan Van Brunt (http://danvanbrunt.com)", - "repository": { - "type": "git", - "url": "git+https://github.com/iDVB/serverless-lambda-version.git" - }, - "keywords": ["serverless", "plugin", "lambda", "edge", "arn", "version"], - "license": "MIT", - "bugs": { - "url": "https://github.com/iDVB/serverless-lambda-version/issues" - }, - "homepage": "https://github.com/iDVB/serverless-lambda-version/blob/master/README.md", - "scripts": { - "eslint": "node node_modules/eslint/bin/eslint.js --ext .js ." - }, - "dependencies": { - "lodash": "^4.17.4" - }, - "devDependencies": { - "babel-eslint": "^7.2.3", - "eslint": "^4.7.2", - "eslint-plugin-import": "^2.7.0", - "eslint-plugin-lodash": "^2.4.5", - "eslint-plugin-promise": "^3.5.0", - "serverless": "^1.52.2" - } -} diff --git a/cla-frontend-contributor-console/Makefile b/cla-frontend-contributor-console/Makefile deleted file mode 100644 index c36f4fef0..000000000 --- a/cla-frontend-contributor-console/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright The Linux Foundation and each contributor to CommunityBridge. -# SPDX-License-Identifier: MIT -.PHONY: setup -setup: - yarn install-frontend - -.PHONY: run -run: - cd src; \ - npm run preserve:${STAGE}; \ - npm run serve:${STAGE}; \ - -.PHONY: deploy -deploy: - yarn deploy -s ${STAGE} -r us-east-1 -c diff --git a/cla-frontend-contributor-console/edge/.gitignore b/cla-frontend-contributor-console/edge/.gitignore deleted file mode 100644 index d185a4baf..000000000 --- a/cla-frontend-contributor-console/edge/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright The Linux Foundation and each contributor to CommunityBridge. -# SPDX-License-Identifier: MIT -dist diff --git a/cla-frontend-contributor-console/edge/jasmine.json b/cla-frontend-contributor-console/edge/jasmine.json deleted file mode 100644 index aec5a224e..000000000 --- a/cla-frontend-contributor-console/edge/jasmine.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "spec_dir": "src", - "spec_files": ["**/*[sS]pec.js"], - "stopSpecOnExpectationFailure": false -} diff --git a/cla-frontend-contributor-console/edge/package.json b/cla-frontend-contributor-console/edge/package.json deleted file mode 100644 index 2ad0aae7c..000000000 --- a/cla-frontend-contributor-console/edge/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "edge", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "scripts": { - "prebuild": "yarn install", - "build": "webpack", - "pretest": "yarn install", - "test": "./node_modules/.bin/jasmine --config=jasmine.json" - }, - "dependencies": { - "graceful-fs": "^4.2.2" - }, - "resolutions": { - "node-sass": "4.13.1", - "mem": "^4.0.0", - "yargs-parser": "13.1.2" - }, - "devDependencies": { - "babel-core": "^6.26.0", - "babel-loader": "^7.1.2", - "jasmine": "^3.1.0", - "webpack": "^3.11.0" - } -} diff --git a/cla-frontend-contributor-console/edge/security-headers.js b/cla-frontend-contributor-console/edge/security-headers.js deleted file mode 100644 index 6ee3be085..000000000 --- a/cla-frontend-contributor-console/edge/security-headers.js +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -function getHeaders(env, isDevServer) { - return { - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', - 'X-XSS-Protection': '1', - 'Referrer-Policy': 'no-referrer', - 'Content-Security-Policy': generateCSP(env, isDevServer), - 'Cache-Control': 's-maxage=31536000' - }; -} - -function getSources(environmentSources, sourceType) { - if (environmentSources[sourceType] === undefined) { - return []; - } - return environmentSources[sourceType].filter((source) => { - return typeof source === 'string'; - }); -} - -function generateCSP(env, isDevServer) { - const SELF = "'self'"; - const UNSAFE_INLINE = "'unsafe-inline'"; - const UNSAFE_EVAL = "'unsafe-eval'"; - const NONE = "'none'"; - - let connectSources = [ - SELF, - 'https://linuxfoundation-dev.auth0.com/', - 'https://linuxfoundation-staging.auth0.com/', - 'https://sso.linuxfoundation.org/', - 'https://api.dev.lfcla.com/', - 'https://api.lfcla.dev.platform.linuxfoundation.org/', - 'https://api.staging.lfcla.com/', - 'https://api.lfcla.staging.platform.linuxfoundation.org/', - 'https://api.lfcla.com/', - 'https://api.easycla.lfx.linuxfoundation.org/', - 'https://communitybridge.org/', - 'https://api-gw.dev.platform.linuxfoundation.org/', - 'https://api-gw.staging.platform.linuxfoundation.org/', - 'https://api-gw.platform.linuxfoundation.org/' - ]; - - let scriptSources = [SELF, UNSAFE_EVAL, UNSAFE_INLINE, - 'https://cdn.dev.platform.linuxfoundation.org/lfx-header-no-zone.js', - 'https://cdn.staging.platform.linuxfoundation.org/lfx-header-no-zone.js', - 'https://cdn.platform.linuxfoundation.org/lfx-header-no-zone.js', - 'https://cdn.dev.platform.linuxfoundation.org/lfx-footer-no-zone.js', - 'https://cdn.staging.platform.linuxfoundation.org/lfx-footer-no-zone.js', - 'https://cdn.platform.linuxfoundation.org/lfx-footer-no-zone.js' - ]; - - let styleSources = [SELF, UNSAFE_INLINE, 'https://communitybridge.org/', 'https://lfx.linuxfoundation.org/']; - - if (isDevServer) { - connectSources = [...connectSources, 'https://localhost:8100/sockjs-node/', 'wss://localhost:8100/sockjs-node/']; - // The webpack dev server uses system js which violates the unsafe-eval exception. This doesn't happen in the - // production AOT build. - // The development build needs unsafe inline assets. - scriptSources = [...scriptSources, UNSAFE_EVAL]; - } - - const CSP_SOURCES = env ? env.CSP_SOURCES : undefined; - const environmentSources = JSON.parse(CSP_SOURCES || '{}'); - - const sources = { - 'default-src': [NONE], - 'img-src': ['*'], // allow all sources - /* - 'img-src': [ - SELF, - 'data:', - '*', - // 'https://s3.amazonaws.com/cla-project-logo-dev/', // project logos - // 'https://s3.amazonaws.com/cla-project-logo-staging/', // project logos - // 'https://s3.amazonaws.com/cla-project-logo-prod/', // project logos - // 'https://s3.amazonaws.com/lf-master-project-logos-prod/', // project logos - // 'https://lf-master-project-logos-prod.s3.us-east-2.amazonaws.com/', // project logos - // 'https://s.gravatar.com/', // my profile user logos - // 'https://lh3.googleusercontent.com/', // my profile user logos - // 'https://platform-logos-myprofile-api-dev.s3.us-east-2.amazonaws.com/', // my profile user logos - // 'https://platform-logos-myprofile-api-staging.s3.us-east-2.amazonaws.com/', // my profile user logos - // 'https://platform-logos-myprofile-api-prod.s3.us-east-2.amazonaws.com/', // my profile user logos - // 'https://avatars3.githubusercontent.com/', // my profile user logos - // 'https://cdn.platform.linuxfoundation.org/', // cdn for the LF favicon: https://cdn.platform.linuxfoundation.org/assets/lf-favicon.png - ], - */ - 'script-src': scriptSources, - 'style-src': styleSources, // Unfortunately using Angular basically requires inline styles. - 'font-src': [SELF, 'data:', 'https://communitybridge.org/'], - 'connect-src': connectSources, - 'frame-ancestors': [NONE], - 'form-action': [NONE], - 'worker-src': [SELF, 'blob:'], - 'base-uri': [SELF], - // frame-src restricts what iframe's you can put on your website - 'frame-src': [ - SELF, - 'data:', - 'https://sso.linuxfoundation.org/', - 'https://cla-signature-files-dev.s3.amazonaws.com/', - 'https://s3.amazonaws.com/cla-project-logo-dev/', - 'https://cla-signature-files-staging.s3.amazonaws.com/', - 'https://s3.amazonaws.com/cla-project-logo-staging/', - 'https://cla-signature-files-prod.s3.amazonaws.com/', - 'https://s3.amazonaws.com/cla-project-logo-prod/', - 'https://linuxfoundation-dev.auth0.com', - 'https://linuxfoundation-staging.auth0.com', - 'https://linuxfoundation.auth0.com' - ], - 'child-src': [], - 'media-src': [], - 'manifest-src': [SELF], - 'object-src': ['data:', '*'] - }; - - return Object.entries(sources) - .map((keyValuePair) => { - const additionalSources = getSources(environmentSources, keyValuePair[0]); - return [keyValuePair[0], [...keyValuePair[1], ...additionalSources]]; - }) - .filter((keyValuePair) => keyValuePair[1].length !== 0) - .map((keyValuePair) => { - const entry = keyValuePair[1].join(' '); - return `${keyValuePair[0]} ${entry};`; - }) - .join(' '); -} - -module.exports = getHeaders; diff --git a/cla-frontend-contributor-console/edge/src/add-headers.js b/cla-frontend-contributor-console/edge/src/add-headers.js deleted file mode 100644 index ceffb1993..000000000 --- a/cla-frontend-contributor-console/edge/src/add-headers.js +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -exports.addHeaders = function(event, headerList) { - const { response } = event.Records[0].cf; - const { headers } = response; - - Object.keys(headerList).forEach((headerName) => { - const headerValue = headerList[headerName]; - headers[headerName.toLowerCase()] = [ - { - key: headerName, - value: headerValue - } - ]; - }); - - return response; -}; diff --git a/cla-frontend-contributor-console/edge/src/add-headers.spec.js b/cla-frontend-contributor-console/edge/src/add-headers.spec.js deleted file mode 100644 index f517449a1..000000000 --- a/cla-frontend-contributor-console/edge/src/add-headers.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -const handler = require('./add-headers'); - -describe('addHeaders', () => { - const EVENT = { - Records: [ - { - cf: { - response: { - headers: { - host: [ - { - value: 'd123.cf.net', - key: 'Host' - } - ] - }, - clientIp: '2001:cdba::3257:9652', - uri: '/index.html', - method: 'GET' - }, - config: { - distributionId: 'EXAMPLE' - } - } - } - ] - }; - - it('returns a response object with added headers', () => { - const headers = { - 'Some-Header-One': '1', - 'Some-Header-Two': '2' - }; - const output = handler.addHeaders(EVENT, headers); - expect(output).toEqual({ - headers: { - host: [ - { - value: 'd123.cf.net', - key: 'Host' - } - ], - 'some-header-one': [ - { - value: '1', - key: 'Some-Header-One' - } - ], - 'some-header-two': [ - { - value: '2', - key: 'Some-Header-Two' - } - ] - }, - clientIp: '2001:cdba::3257:9652', - uri: '/index.html', - method: 'GET' - }); - }); -}); diff --git a/cla-frontend-contributor-console/edge/src/index.js b/cla-frontend-contributor-console/edge/src/index.js deleted file mode 100644 index 33eb978e8..000000000 --- a/cla-frontend-contributor-console/edge/src/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -const addHeaders = require('./add-headers'); -const setCacheControl = require('./set-cache-control'); - -exports.handler = (event, context, callback) => { - const headers = HEADERS; - const resourcesNotToCache = ['/index.html', '/']; - const resource = event.Records[0].cf.request.uri; - const timeToLive = 60 * 60 * 24 * 365; - const modifiedHeaders = setCacheControl.setCacheControl(headers, resource, resourcesNotToCache, timeToLive); - const response = addHeaders.addHeaders(event, modifiedHeaders); - callback(null, response); -}; diff --git a/cla-frontend-contributor-console/edge/src/set-cache-control.js b/cla-frontend-contributor-console/edge/src/set-cache-control.js deleted file mode 100644 index abe5f3936..000000000 --- a/cla-frontend-contributor-console/edge/src/set-cache-control.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -/** - * Splits a header property in the format 'key=value', or just 'key', and returns an object in the format {key: value}. - * @param {string} assignment - */ -function splitAssignmentPair(assignment) { - const parts = assignment.split('=').map((value) => value.trim()); - - const obj = {}; - if (parts.length == 1) { - obj[parts[0]] = true; - } else { - obj[parts[0]] = parts[1]; - } - return obj; -} - -/** - * Splits a comma seperated header into a list of key-value pairs. - * @param {string} headerValue - */ -function splitHeaderValue(headerValue) { - return headerValue - .split(',') - .map((value) => splitAssignmentPair(value)) - .reduce((previous, current) => Object.assign(previous, current), {}); -} - -/** - * Modifies the Cache-Control header on a per resource basis. - * @param {Object.} headers - A list of preset headers. - * @param {String} currentResourceName - The name of the current resource. - * @param {Array} resourcesNotToCache - A list of resources not to cache. - * @param {Number} timeToLive - The time to cache objects for, if they are to be cached. - */ -exports.setCacheControl = function(headers, currentResourceName, resourcesNotToCache, timeToLive) { - const existingCacheControl = headers['Cache-Control'] !== undefined ? headers['Cache-Control'] : ''; - const cacheValues = splitHeaderValue(existingCacheControl); - const sMaxAge = cacheValues['s-maxage']; - - let newCacheControl = ''; - if (resourcesNotToCache.includes(currentResourceName)) { - newCacheControl = 'no-cache, no-store, must-revalidate'; - } else { - newCacheControl = `max-age=${timeToLive}`; - } - if (sMaxAge) { - newCacheControl = `s-maxage=${sMaxAge}, ${newCacheControl}`; - } - - return Object.assign({}, headers, { 'Cache-Control': newCacheControl }); -}; diff --git a/cla-frontend-contributor-console/edge/src/set-cache-control.spec.js b/cla-frontend-contributor-console/edge/src/set-cache-control.spec.js deleted file mode 100644 index 9cee1f266..000000000 --- a/cla-frontend-contributor-console/edge/src/set-cache-control.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -const handler = require('./set-cache-control'); - -describe('setCacheControl', () => { - it("turns on Cache-Control with max-age when resource isn't in filesNotToCache", () => { - const headers = { - 'Some-Header-One': '1', - 'Some-Header-Two': '2' - }; - const filesNotToCache = ['index.html']; - const timeToLive = 60 * 60 * 24 * 365; - - const result = handler.setCacheControl(headers, 'some-image.png', filesNotToCache, timeToLive); - expect(result).toEqual({ - ...headers, - 'Cache-Control': `max-age=${timeToLive}` - }); - }); - - it('turns off Cache-Control when resource is in filesNotToCache', () => { - const headers = { - 'Some-Header-One': '1', - 'Some-Header-Two': '2' - }; - const filesNotToCache = ['index.html']; - const timeToLive = 60 * 60 * 24 * 365; - - const result = handler.setCacheControl(headers, 'index.html', filesNotToCache, timeToLive); - expect(result).toEqual({ - ...headers, - 'Cache-Control': `no-cache, no-store, must-revalidate` - }); - }); - - it("doesn't change the s-maxage property in an existing Cache-Control header", () => { - const headers = { - 'Some-Header-One': '1', - 'Some-Header-Two': '2', - 'Cache-Control': 's-maxage=100' - }; - const filesNotToCache = ['index.html']; - const timeToLive = 60 * 60 * 24 * 365; - - const result = handler.setCacheControl(headers, 'index.html', filesNotToCache, timeToLive); - expect(result).toEqual({ - ...headers, - 'Cache-Control': `s-maxage=100, no-cache, no-store, must-revalidate` - }); - }); - - it('overrides max-age property in an existing Cache-Control header', () => { - const headers = { - 'Some-Header-One': '1', - 'Some-Header-Two': '2', - 'Cache-Control': 'max-age=100' - }; - const filesNotToCache = ['index.html']; - const timeToLive = 60 * 60 * 24 * 365; - - const result = handler.setCacheControl(headers, 'some-file.html', filesNotToCache, timeToLive); - expect(result).toEqual({ - ...headers, - 'Cache-Control': `max-age=${timeToLive}` - }); - }); -}); diff --git a/cla-frontend-contributor-console/edge/webpack.config.js b/cla-frontend-contributor-console/edge/webpack.config.js deleted file mode 100644 index 4d30f7d47..000000000 --- a/cla-frontend-contributor-console/edge/webpack.config.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -const webpack = require('webpack'); -const path = require('path'); - -const GetSecurityHeaders = require('./security-headers.js'); - -module.exports = (env) => { - const securityHeaders = GetSecurityHeaders(env, false); - - return { - target: 'node', - context: path.join(__dirname, '/src'), - entry: { - index: ['./index.js'] - }, - output: { - path: path.join(__dirname, './dist'), - filename: '[name].js', - libraryTarget: 'umd' - }, - module: { - rules: [ - { - test: /\.js$/, - loaders: ['babel-loader'] - } - ] - }, - plugins: [ - new webpack.DefinePlugin({ - HEADERS: JSON.stringify(securityHeaders) - }) - ] - }; -}; diff --git a/cla-frontend-contributor-console/edge/yarn.lock b/cla-frontend-contributor-console/edge/yarn.lock deleted file mode 100644 index 726831108..000000000 --- a/cla-frontend-contributor-console/edge/yarn.lock +++ /dev/null @@ -1,3487 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn-dynamic-import@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" - integrity sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ= - dependencies: - acorn "^4.0.3" - -acorn@^4.0.3: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= - -acorn@^5.0.0: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== - -ajv-keywords@^3.1.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957" - integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA== - -ajv@^6.1.0: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== - 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" - -ajv@^6.12.3: - version "6.12.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== - 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" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -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== - 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== - -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" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -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.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1.js@^4.0.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -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= - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= - -async@^2.1.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -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.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-loader@^7.1.2: - version "7.1.5" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68" - integrity sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw== - dependencies: - find-cache-dir "^1.0.0" - loader-utils "^1.0.2" - mkdirp "^0.5.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -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-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -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= - dependencies: - tweetnacl "^0.14.3" - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -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== - -bindings@^1.5.0: - 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" - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" - integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^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== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" - integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.2" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" - integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== - 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.4.0" - optionalDependencies: - fsevents "~2.1.2" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.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" - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -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-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -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= - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -convert-source-map@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.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= - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.11" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" - integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== - -core-util-is@1.0.2, 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" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -create-ecdh@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" - integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== - dependencies: - bn.js "^4.1.0" - elliptic "^6.0.0" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^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" - 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= - dependencies: - assert-plus "^1.0.0" - -debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -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= - -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= - dependencies: - is-descriptor "^0.1.0" - -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-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== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.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= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -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" - -elliptic@^6.0.0, elliptic@^6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -enhanced-resolve@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" - integrity sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24= - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - object-assign "^4.0.1" - tapable "^0.2.7" - -errno@^0.1.3: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" - -es6-iterator@^2.0.3, es6-iterator@~2.0.1, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-map@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA= - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - -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= - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-symbol@^3.1.1, 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" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -es6-weak-map@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" - integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== - dependencies: - d "1" - es5-ext "^0.10.46" - es6-iterator "^2.0.3" - es6-symbol "^3.1.1" - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" - integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM= - dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - -estraverse@^4.1.0, estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= - dependencies: - d "1" - es5-ext "~0.10.14" - -events@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" - integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -ext@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== - dependencies: - type "^2.0.0" - -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= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -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== - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -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: - 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-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== - -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== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -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== - dependencies: - to-regex-range "^5.0.1" - -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -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= - -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== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -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= - 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" - -gaze@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -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-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -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" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.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== - dependencies: - is-glob "^4.0.1" - -glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - 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" - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -globule@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4" - integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.2: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -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-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= - -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-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== - -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" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -in-publish@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.1.tgz#948b1a535c8030561cea522f73f78f4be357e00c" - integrity sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ== - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -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= - dependencies: - kind-of "^3.0.2" - -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== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - 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= - dependencies: - kind-of "^3.0.2" - -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-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== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -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== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -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-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -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-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -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= - -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-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@1.0.0, 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= - -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= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -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= - -jasmine-core@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4" - integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA== - -jasmine@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.5.0.tgz#7101eabfd043a1fc82ac24e0ab6ec56081357f9e" - integrity sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ== - dependencies: - glob "^7.1.4" - jasmine-core "~3.5.0" - -js-base64@^2.1.8: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -json-loader@^0.5.4: - version "0.5.7" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" - integrity sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w== - -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-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= - -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -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" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - 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@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - 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== - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -loader-runner@^2.3.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -lodash@^4.0.0, lodash@^4.17.15, lodash@~4.17.10: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -lodash@^4.17.14, lodash@^4.17.4: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -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= - dependencies: - object-visit "^1.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mem@^1.1.0, mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.0, memory-fs@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -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== - -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== - dependencies: - mime-db "1.44.0" - -mimic-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4, minimatch@~3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.1.3, 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== - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -nan@^2.12.1, nan@^2.13.2: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -neo-async@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -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= - -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - -node-libs-browser@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-sass@4.13.1: - version "4.13.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" - integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash "^4.17.15" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.13.2" - node-gyp "^3.8.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "^2.2.4" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -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" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - 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== - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -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-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= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA== - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - -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@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" - integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== - dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -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= - -picomatch@^2.0.4, 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== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -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= - -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= - 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= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -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" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -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== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -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== - -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== - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.3.3, readable-stream@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - 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.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== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== - dependencies: - picomatch "^2.2.1" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request@^2.87.0, 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-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.10.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - -rimraf@2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, 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-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" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass-graph@^2.2.4: - version "2.2.6" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.6.tgz#09fda0e4287480e3e4967b72a2d133ba09b8d827" - integrity sha512-MKuEYXFSGuRSi8FZ3A7imN1CeVn9Gpw0/SFJKdL1ejXJneI9a5rwlEZrKejhEFAA3O6yr3eIyl/WuvASvlT36g== - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^7.0.0" - -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - -"semver@2 || 3 || 4 || 5": - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - -set-blocking@^2.0.0, 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-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -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= - 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= - -signal-exit@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -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" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stdout-stream@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" - integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== - dependencies: - readable-stream "^2.0.1" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^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" - 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" - 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-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^4.2.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" - integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s= - dependencies: - has-flag "^2.0.0" - -tapable@^0.2.7: - version "0.2.9" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.9.tgz#af2d8bbc9b04f74ee17af2b4d9048f807acd18a8" - integrity sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A== - -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - -timers-browserify@^2.0.4: - version "2.0.11" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" - integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== - dependencies: - setimmediate "^1.0.4" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -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-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -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" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -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== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -"true-case-path@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" - integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== - dependencies: - glob "^7.1.2" - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -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= - -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3" - integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow== - -uglify-js@^2.8.29: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - -uglifyjs-webpack-plugin@^0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" - integrity sha1-uVH0q7a9YX5m9j64kUmOORdj4wk= - dependencies: - source-map "^0.5.6" - uglify-js "^2.8.29" - webpack-sources "^1.0.1" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -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" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -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" - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -watchpack-chokidar2@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" - integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.4.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" - integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.0" - watchpack-chokidar2 "^2.0.0" - -webpack-sources@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.12.0.tgz#3f9e34360370602fcf639e97939db486f4ec0d74" - integrity sha512-Sw7MdIIOv/nkzPzee4o0EdvCuPmxT98+vVpIvwtcwcF1Q4SDSNp92vwcKc4REe7NItH9f1S4ra9FuQ7yuYZ8bQ== - dependencies: - acorn "^5.0.0" - acorn-dynamic-import "^2.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - async "^2.1.2" - enhanced-resolve "^3.4.0" - escope "^3.6.0" - interpret "^1.0.0" - json-loader "^0.5.4" - json5 "^0.5.1" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - mkdirp "~0.5.0" - node-libs-browser "^2.0.0" - source-map "^0.5.3" - supports-color "^4.2.1" - tapable "^0.2.7" - uglifyjs-webpack-plugin "^0.4.6" - watchpack "^1.4.0" - webpack-sources "^1.0.1" - yargs "^8.0.2" - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@1, which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -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== - dependencies: - string-width "^1.0.2 || 2" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yargs-parser@13.1.2, yargs-parser@5.0.0-security.0, yargs-parser@^7.0.0: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6" - integrity sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g== - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "5.0.0-security.0" - -yargs@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" - integrity sha1-YpmpBVsc78lp/355wdkY3Osiw2A= - dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" diff --git a/cla-frontend-contributor-console/package.json b/cla-frontend-contributor-console/package.json deleted file mode 100644 index 2a31baa5a..000000000 --- a/cla-frontend-contributor-console/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "cla-frontend-contributor-console", - "version": "0.0.0", - "license": "MIT", - "scripts": { - "deploy:dev": "yarn sls deploy --stage=dev --cloudfront=true && yarn sls client deploy --stage=dev --cloudfront=true --no-confirm --no-policy-change --no-config-change && yarn sls cloudfrontInvalidate --stage=dev --region=us-east-1 --cloudfront=true", - "deploy:staging": "yarn sls deploy --stage=staging --cloudfront=true && yarn sls client deploy --stage=staging --cloudfront=true --no-confirm --no-policy-change --no-config-change && yarn sls cloudfrontInvalidate --stage=staging --region=us-east-1 --cloudfront=true", - "deploy:prod": "SLS_DEBUG=* yarn sls client deploy --stage='prod' --region='us-east-1' --cloudfront=true --no-confirm --no-policy-change --no-config-change --verbose && SLS_DEBUG=* yarn sls deploy --stage='prod' --region='us-east-1' --cloudfront=true --verbose && SLS_DEBUG=* yarn sls cloudfrontInvalidate --stage='prod' --region='us-east-1' --cloudfront='true' --verbose", - "sls": "../node_modules/serverless/bin/serverless.js", - "info:dev": "../node_modules/serverless/bin/serverless.js info --stage=dev --region=us-east-1", - "info:stating": "../node_modules/serverless/bin/serverless.js info --stage=stating --region=us-east-1", - "info:prod": "../node_modules/serverless/bin/serverless.js info --stage=prod --region=us-east-1", - "install-frontend": "../scripts/install-frontend.sh" - }, - "dependencies": { - "graceful-fs": "^4.2.2", - "ionic": "^3.20.0", - "serverless": "^2.15.0", - "serverless-cloudfront-invalidate": "^1.2.1", - "serverless-finch": "^2.6.0", - "serverless-plugin-tracing": "^2.0.0", - "serverless-pseudo-parameters": "^2.5.0" - }, - "resolutions": { - "axios": "^0.21.1", - "bl": "^2.2.1", - "ini": "^1.3.7" - } -} diff --git a/cla-frontend-contributor-console/serverless.yml b/cla-frontend-contributor-console/serverless.yml deleted file mode 100644 index 935e03a37..000000000 --- a/cla-frontend-contributor-console/serverless.yml +++ /dev/null @@ -1,280 +0,0 @@ -service: cla-frontend-ic-4 - -# Only package lambda@edge function. -package: - exclude: - - "**" - include: - - edge/dist/* - -provider: - name: aws - #runtime: nodejs8.10 # https://aws.amazon.com/about-aws/whats-new/2018/05/lambda-at-edge-adds-support-for-node-js-v8-10/ - runtime: nodejs12.x - region: us-east-1 # Region can't be configurable, lambda@edge is us-east-1 only. - deploymentBucket: - serverSideEncryption: AES256 # Make sure items are uploaded encrypted. - role: EdgeRole - - tracing: - apiGateway: true - lambda: true - - iamRoleStatements: - - Effect: Allow - Action: - - xray:PutTraceSegments - - xray:PutTelemetryRecords - Resource: "*" - -plugins: - # Serverless Finch does s3 uploading. Called with 'sls client deploy'. - # Also allows bucket removal with 'sls client remove'. - - serverless-finch - # Automatically versions and updates the lambda@edge function. - - serverless-lambda-version - # Automatically invalidates cloudfront after frontend bucket is deployed - - serverless-cloudfront-invalidate - - serverless-plugin-tracing - -custom: - project: ${file(../project-vars.yml):projectIdentifier} - client: # Configurations for serverless finch. - bucketName: ${self:custom.project}-${opt:stage}-${self:service} - distributionFolder: src/www - indexDocument: index.html - # Because our application is a Single Page Application, we always want our index - # documents to handle 404/403 urls. - errorDocument: index.html - manageResources: false - - # CloudFront invalidation plugin configuration - cloudfrontInvalidate: - # Grab the distribution ID key from the output section - distributionIdKey: 'CloudfrontDistributionId' - items: # one or more paths required - - '/*' - certificate: - arn: - # From env Certificate Manager - - # currently, PROD is managed externally, DEV and STAGING are still managed by serverless - dev: arn:aws:acm:us-east-1:395594542180:certificate/b6efe7d2-d8c3-4d27-a582-341a8df70ccd - staging: arn:aws:acm:us-east-1:844390194980:certificate/dc289a7d-8410-452e-860b-a4ccadd99fad - prod: arn:aws:acm:us-east-1:716487311010:certificate/0fad9da1-45c3-46ba-95bb-36e62a20a572 - other: arn:aws:acm:us-east-1:395594542180:certificate/b6efe7d2-d8c3-4d27-a582-341a8df70ccd - product: - domain: - name: - dev: 'contributor.lfcla.dev.platform.linuxfoundation.org' - staging: 'contributor.lfcla.staging.platform.linuxfoundation.org' - prod: 'contributor.v1.easycla.lfx.linuxfoundation.org' - other: 'contributor.dev.lfcla.com' - alt: - dev: 'contributor.dev.lfcla.com' - staging: 'contributor.staging.lfcla.com' - prod: 'contributor.v1.easycla.lfx.linuxfoundation.org' - other: 'contributor.dev.lfcla.com' - ses_from_email: - dev: admin@dev.lfcla.com - staging: admin@staging.lfcla.com - prod: admin@lfx.linuxfoundation.org - -functions: - # Configure a lambda@edge handler. Lambda@edge is a function that adds http headers to - # cloudfront requests. We use it to enforce HTTP security best practices. - clientEdge: - handler: edge/dist/index.handler - memorySize: 128 # This is the maximum memory size for lambda@edge functions - timeout: 1 # This is the maximum execution time for lambda@edge functions - -resources: - Conditions: - # https://gist.github.com/DavidWells/be078deef45f8cb2e280ccc7af947392 - isProd: { "Fn::Equals": [ "${env:STAGE}", "prod" ] } - isStaging: { "Fn::Equals": [ "${env:STAGE}", "staging" ] } - isDev: { "Fn::Equals": [ "${env:STAGE}", "dev" ] } - # true when a TSL certificate should be created by serverless (false created externally) - ShouldGenerateCertificate: - Fn::Not: [Fn::Equals: ["${env:STAGE}", "prod"]] - - Resources: - # The bucket the website is uploaded to. We make sure to turn on AES256 encryption, which - # is best practice. - WebsiteDeploymentBucket: - Type: AWS::S3::Bucket - Properties: - AccessControl: Private - BucketEncryption: - ServerSideEncryptionConfiguration: - - ServerSideEncryptionByDefault: - SSEAlgorithm: AES256 - BucketName: ${self:custom.project}-${opt:stage}-${self:service} - - # Policy that only exposes bucket to cloudfront with proper - # Origin Access Identity - WebsiteDeploymentBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: - Ref: WebsiteDeploymentBucket - PolicyDocument: - Statement: - - Action: - - "s3:GetObject" - Effect: Allow - Resource: - "Fn::Join": - - "" - - - "arn:aws:s3:::" - - Ref: WebsiteDeploymentBucket - - "/*" - Principal: - AWS: - "Fn::Join": - - " " - - - "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity" - - Ref: WebsiteOriginAccessIdentity - - WebsiteOriginAccessIdentity: - Type: AWS::CloudFront::CloudFrontOriginAccessIdentity - Properties: - CloudFrontOriginAccessIdentityConfig: - Comment: "CloudFrontOriginAccessIdentity for ${self:service}-${self:provider.stage}" - - # The cloudfront distribution wraps around our static website S3 bucket. Using a CDN to host our SPA is good - # practice, and also lets us set custom headers using lambda@edge. - CloudfrontDistribution: - Type: AWS::CloudFront::Distribution - DependsOn: - - WebsiteDeploymentBucket - Properties: - DistributionConfig: - Enabled: true - Aliases: - Fn::If: - - isProd - - - ${self:custom.product.domain.name.${opt:stage}, self:custom.product.domain.name.other} - - - ${self:custom.product.domain.name.${opt:stage}, self:custom.product.domain.name.other} - - ${self:custom.product.domain.alt.${opt:stage}, self:custom.product.domain.alt.other} - ViewerCertificate: - Fn::If: - - ShouldGenerateCertificate - - AcmCertificateArn: - Ref: Cert - # The distribution accepts HTTPS connections from only viewers that support server name indication - # Recommended, most browsers and clients released after 2010 support SNI. - SslSupportMethod: sni-only - # Specify the security policy that you want CloudFront to use for HTTPS connections - # Recommend that you specify TLSv1.2_2018 unless your viewers are using browsers or devices that don’t support TLSv1.2 - # Allowed Values: SSLv3 | TLSv1 | TLSv1.1_2016 | TLSv1.2_2018 | TLSv1_2016 - MinimumProtocolVersion: TLSv1.2_2018 - - AcmCertificateArn: ${self:custom.certificate.arn.${opt:stage}, self:custom.certificate.arn.other} - # The distribution accepts HTTPS connections from only viewers that support server name indication - # Recommended, most browsers and clients released after 2010 support SNI. - SslSupportMethod: sni-only - # Specify the security policy that you want CloudFront to use for HTTPS connections - # Recommend that you specify TLSv1.2_2018 unless your viewers are using browsers or devices that don’t support TLSv1.2 - # Allowed Values: SSLv3 | TLSv1 | TLSv1.1_2016 | TLSv1.2_2018 | TLSv1_2016 - MinimumProtocolVersion: TLSv1.2_2018 - Origins: - - DomainName: { "Fn::GetAtt": [ WebsiteDeploymentBucket, DomainName ] } - Id: - Ref: WebsiteDeploymentBucket - S3OriginConfig: - OriginAccessIdentity: - "Fn::Join": - - "" - - - "origin-access-identity/cloudfront/" - - Ref: WebsiteOriginAccessIdentity - # Routes besides / will result in S3 serving a 403 - # Redirect all routes back to the SPA where routes should - # be handled - CustomErrorResponses: - - - ErrorCode: 403 - ErrorCachingMinTTL: 1 - ResponseCode: 200 - ResponsePagePath: '/index.html' - HttpVersion: http2 - DefaultRootObject: index.html - DefaultCacheBehavior: - AllowedMethods: - - GET - - HEAD - # Links our lambda@edge function, (which adds HTTPS our security headers), to the cloudfront distribution. - LambdaFunctionAssociations: - - EventType: 'viewer-response' - # Cloudfront requires a lambda@edge arn in the format - # 'arn:aws:lambda:${region}:${accountNumber}:function:${lambdaName}:${explicitVersion}' - # We use the serverless-lambda-version plugin to automatically update this every time there is a change. - LambdaFunctionARN: ClientEdgeLambdaFunction - Compress: true # Turns on gzipping - #DefaultTTL: 86400 # Defaults to a day if no Cache-Control header is set. - DefaultTTL: 600 # 10 minutes only due to users seeing a lot of stale cache issues after release (even after invalidating - MinTTL: 0 - #MaxTTL: 31536000 # Can keep the file in the cloudfront cache for a maximum of a year. - MaxTTL: 600 # 10 minutes only due to users seeing a lot of stale cache issues after release (even after invalidating - TargetOriginId: - Ref: WebsiteDeploymentBucket - ForwardedValues: - QueryString: true - Cookies: - Forward: none - ViewerProtocolPolicy: redirect-to-https - PriceClass: PriceClass_100 # Cheapest class, only hosts content at North American cloudfront locations. - - # Severless usually generates our roles out of the box, but lambda@edge support is lacking, so we have to create - # our own. This role can assume the edgelambda.amazonaws.com role, (the lambda won't run without it). - EdgeRole: - Type: AWS::IAM::Role - Properties: - RoleName: ${self:custom.project}-${opt:stage}-${self:service}-edge-role - Path: / - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - - edgelambda.amazonaws.com # This is the important part of this role. - Action: - - sts:AssumeRole - Policies: - - PolicyName: LogGroupPolicy # Permissions to access Lambda@edge log groups. - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:DescribeLogGroups - - logs:DescribeLogStreams - - logs:PutLogEvents - - logs:GetLogEvents - - logs:FilterLogEvents - Resource: - - "Fn::Join": - - ":" - - - arn:aws:logs - - "Ref": "AWS::Region" - - "Ref": "AWS::AccountId" - - log-group - - "*" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - - Cert: - Type: AWS::CertificateManager::Certificate - Condition: ShouldGenerateCertificate - Properties: - DomainName: ${self:custom.product.domain.name.${opt:stage}, self:custom.product.domain.name.other} - SubjectAlternativeNames: - - ${self:custom.product.domain.alt.${opt:stage}, self:custom.product.domain.alt.other} - ValidationMethod: DNS - - Outputs: - CloudfrontDistributionId: - Value: - Ref: CloudfrontDistribution diff --git a/cla-frontend-contributor-console/src/.editorconfig b/cla-frontend-contributor-console/src/.editorconfig deleted file mode 100755 index 51873bc7f..000000000 --- a/cla-frontend-contributor-console/src/.editorconfig +++ /dev/null @@ -1,17 +0,0 @@ -# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs -# editorconfig.org - -root = true - -[*] -indent_style = space -indent_size = 2 - -# We recommend you to keep these unchanged -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/cla-frontend-contributor-console/src/.io-config.json b/cla-frontend-contributor-console/src/.io-config.json deleted file mode 100755 index 141a2bbf3..000000000 --- a/cla-frontend-contributor-console/src/.io-config.json +++ /dev/null @@ -1 +0,0 @@ -{"app_id":"3a1139c5"} \ No newline at end of file diff --git a/cla-frontend-contributor-console/src/config.xml b/cla-frontend-contributor-console/src/config.xml deleted file mode 100755 index 973d6de92..000000000 --- a/cla-frontend-contributor-console/src/config.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - cla-app - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cla-frontend-contributor-console/src/config/scripts/prefetch-ssm.js b/cla-frontend-contributor-console/src/config/scripts/prefetch-ssm.js deleted file mode 100644 index 7115da515..000000000 --- a/cla-frontend-contributor-console/src/config/scripts/prefetch-ssm.js +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -const fs = require('fs'); -const RetrieveSSMValues = require('./read-ssm'); -const configVarArray = ['auth0-clientId', 'auth0-domain', 'cla-api-url', 'corp-console-link', 'logo-url', 'lfx-header', 'lfx-footer', 'lfx-header-enabled', 'landing-page','auth0-platform-api-gw']; -const region = 'us-east-1'; -const profile = process.env.AWS_PROFILE; -const stageEnv = process.env.STAGE_ENV; - -async function prefetchSSM() { - let result = {}; - console.log(`Start to fetch SSM values at ${stageEnv}...`); - result = await RetrieveSSMValues(configVarArray, stageEnv, region, profile); - - //test for local - // result['cla-api-url'] = 'http://localhost:5000'; - fs.writeFile(`./config/cla-env-config.json`, JSON.stringify(result), function (err) { - if (err) throw new Error(`Couldn't save SSM paramters to disk with error ${err}`); - console.log('Fetching completed...'); - }); -} - -prefetchSSM(); diff --git a/cla-frontend-contributor-console/src/config/scripts/read-local.js b/cla-frontend-contributor-console/src/config/scripts/read-local.js deleted file mode 100644 index a611c366e..000000000 --- a/cla-frontend-contributor-console/src/config/scripts/read-local.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -/** - * @param {string[]} variables - * @returns {{ [key:string]: string }} - */ -async function retrieveLocalConfigValues(variables, fileName) { - const localConfig = require(`../${fileName}`); - const parameterMap = {}; - variables.forEach((variable) => { - value = localConfig[variable]; - if (value === undefined) { - throw new Error(`Couldn't retrieve value from local config for ${variable}`); - } - parameterMap[variable] = localConfig[variable]; - }); - return parameterMap; -} - -module.exports = retrieveLocalConfigValues; diff --git a/cla-frontend-contributor-console/src/config/scripts/read-ssm.js b/cla-frontend-contributor-console/src/config/scripts/read-ssm.js deleted file mode 100644 index d3d515e51..000000000 --- a/cla-frontend-contributor-console/src/config/scripts/read-ssm.js +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-check - -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT -const AWS = require('aws-sdk'); - -/** - * @param {string[]} variables - * @param {string} stage - * @param {string} region - * @param {string} profile - * @returns {Promise<{ [key:string]: string}>} - */ -async function retrieveSSMValues(variables, stage, region, profile) { - const scopedVariables = variables.map((param) => { - return `cla-${param}-${stage}`; - }); - - const result = await requestSSMParameters(scopedVariables, stage, region, profile); - const parameters = result.Parameters; - const error = result.$response.error; - if (error !== null) { - throw new Error( - `Couldn't retrieve SSM parameters for stage ${stage} in region ${region} using profile ${profile} - error ${error}` - ); - } - const scopedParams = createParameterMap(parameters, stage); - let params = {}; - Object.keys(scopedParams).forEach((key) => { - const param = scopedParams[key]; - key = key.replace('cla-', ''); - key = key.replace(`-${stage}`, ''); - params[key] = param; - }); - - variables.forEach((variable) => { - if (params[variable] === undefined) { - throw new Error( - `Missing SSM parameter with name ${variable} for stage ${stage} in region ${region} using profile ${profile}`, - ); - } - }); - return params; -} - -/** - * @param {string[]} variables - * @param {string} stage - * @param {string} region - */ -function requestSSMParameters(variables, stage, region, profile) { - AWS.config.credentials = new AWS.SharedIniFileCredentials({profile}); - const ssm = new AWS.SSM({region: region}); - - const ps = { - Names: variables, - WithDecryption: true - }; - - return ssm.getParameters(ps).promise(); -} - -/** - * @param {AWS.SSM.Parameter[]} parameters - * @param {string} stage - */ -function createParameterMap(parameters, stage) { - return parameters.filter((param) => param.Name.endsWith(`-${stage}`)) - .map((param) => { - const output = {}; - output[param.Name] = param.Value; - return output; - }) - .reduce((prev, current) => { - return {...prev, ...current}; - }, {}); -} - -module.exports = retrieveSSMValues; diff --git a/cla-frontend-contributor-console/src/config/webpack.config.js b/cla-frontend-contributor-console/src/config/webpack.config.js deleted file mode 100644 index ff0b4a2e9..000000000 --- a/cla-frontend-contributor-console/src/config/webpack.config.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -const { dev, prod } = require('@ionic/app-scripts/config/webpack.config'); -const webpack = require('webpack'); -const RetrieveLocalConfigValues = require('./scripts/read-local'); -const configVarArray = ['auth0-clientId', 'auth0-domain', 'cla-api-url']; -const stageEnv = process.env.STAGE_ENV; -/** - * This custom webpack config is deprecated, - * since we don't inject environment variables through webpack plugin. - * If we're going to reactivate this someday, go to src/package.json, - * add `"ionic_webpack": "./config/webpack.config.js"` at config block. - */ -module.exports = async (env) => { - // Here we hard code stage name, it's not perfect since if a new stage created/modified, we also need to change it. - const shouldReadFromSSM = - stageEnv !== undefined && - (stageEnv === 'staging' || stageEnv === 'prod' || stageEnv === 'qa' || stageEnv === 'dev'); - let configMap = {}; - - // Here in the future, we maybe want to use Enum class to replace hard-code file name as indicator. - if (shouldReadFromSSM) { - configMap = await RetrieveLocalConfigValues(configVarArray, `config-${stageEnv}.json`); - } else { - configMap = await RetrieveLocalConfigValues(configVarArray, 'config-local.json'); - } - - const claConfigPlugin = new webpack.DefinePlugin({ - webpackGlobalVars: { - CLA_API_URL: JSON.stringify(configMap['cla-api-url']), - AUTH0_DOMAIN: JSON.stringify(configMap['auth0-domain']), - AUTH0_CLIENT_ID: JSON.stringify(configMap['auth0-clientId']) - } - }); - - dev.plugins.push(claConfigPlugin); - prod.plugins.push(claConfigPlugin); - - return { - dev: dev, - prod: prod - }; -}; diff --git a/cla-frontend-contributor-console/src/ionic.config.json b/cla-frontend-contributor-console/src/ionic.config.json deleted file mode 100755 index 4d12d43f0..000000000 --- a/cla-frontend-contributor-console/src/ionic.config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "cla-console-app", - "app_id": "3a1139c5", - "type": "ionic-angular", - "integrations": {} -} diff --git a/cla-frontend-contributor-console/src/ionic/app/app.component.ts b/cla-frontend-contributor-console/src/ionic/app/app.component.ts deleted file mode 100755 index ded4a60fb..000000000 --- a/cla-frontend-contributor-console/src/ionic/app/app.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component, ViewChild } from '@angular/core'; -import { Nav, Platform } from 'ionic-angular'; -import { StatusBar } from '@ionic-native/status-bar'; -import { SplashScreen } from '@ionic-native/splash-screen'; -import { ClaService } from '../services/cla.service'; -import { EnvConfig } from '../services/cla.env.utils'; - -import { AuthService } from '../services/auth.service'; -import { AuthPage } from '../pages/auth/auth'; -import { HttpClient } from '../services/http-client'; -import { KeycloakService } from '../services/keycloak/keycloak.service'; -import { LfxHeaderService } from '../services/lfx-header.service'; - -@Component({ - templateUrl: 'app.html', - providers: [] -}) -export class MyApp { - @ViewChild(Nav) nav: Nav; - - rootPage: any = AuthPage; - - constructor( - public platform: Platform, - public statusBar: StatusBar, - public splashScreen: SplashScreen, - public claService: ClaService, - public authService: AuthService, - public httpClient: HttpClient, - public keycloak: KeycloakService, - private lfxHeaderService: LfxHeaderService - ) { - this.initializeApp(); - - // Determine if we're running in a local services (developer) mode - the USE_LOCAL_SERVICES environment variable - // will be set to 'true', otherwise we're using normal services deployed in each environment - const localServicesMode = (process.env.USE_LOCAL_SERVICES || 'false').toLowerCase() === 'true'; - // Set true for local debugging using localhost (local ports set in claService) - this.claService.isLocalTesting(localServicesMode); - this.claService.setApiUrl(EnvConfig['cla-api-url']); - this.claService.setV4ApiUrl(EnvConfig['auth0-platform-api-gw']); - this.claService.setHttp(httpClient); - } - - ngOnInit() { - this.mountHeader(); - this.mountFooter(); - } - - initializeApp() { - this.platform.ready().then(() => { }); - } - - mountHeader() { - const script = document.createElement('script'); - script.setAttribute( - 'src', - EnvConfig['lfx-header'] - ); - document.head.appendChild(script); - } - - mountFooter() { - const script = document.createElement('script'); - script.setAttribute( - 'src', - EnvConfig['lfx-footer'] - ); - document.head.appendChild(script); - } - - openPage(page) { - // Set the nav root so back button doesn't show - this.nav.setRoot(page.component); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/app/app.html b/cla-frontend-contributor-console/src/ionic/app/app.html deleted file mode 100755 index 3d7474c1c..000000000 --- a/cla-frontend-contributor-console/src/ionic/app/app.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/cla-frontend-contributor-console/src/ionic/app/app.module.ts b/cla-frontend-contributor-console/src/ionic/app/app.module.ts deleted file mode 100755 index df1fde965..000000000 --- a/cla-frontend-contributor-console/src/ionic/app/app.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule, ErrorHandler } from '@angular/core'; -import { HttpModule } from '@angular/http'; -import { CurrencyPipe } from '@angular/common'; -import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; -import { StatusBar } from '@ionic-native/status-bar'; -import { SplashScreen } from '@ionic-native/splash-screen'; -import { ClaService } from '../services/cla.service'; -import { AuthService } from '../services/auth.service'; -import { LfxHeaderService } from '../services/lfx-header.service'; -import { RolesService } from '../services/roles.service'; -import { HttpClient } from '../services/http-client'; -import { KeycloakService } from '../services/keycloak/keycloak.service'; -import { KeycloakHttp, KEYCLOAK_HTTP_PROVIDER } from '../services/keycloak/keycloak.http'; -import { AuthPage } from '../pages/auth/auth'; -import { MyApp } from './app.component'; -import { LayoutModule } from '../layout/layout.module'; - -@NgModule({ - declarations: [MyApp, AuthPage], - imports: [BrowserModule, HttpModule, LayoutModule, IonicModule.forRoot(MyApp)], - bootstrap: [IonicApp], - entryComponents: [MyApp, AuthPage], - providers: [ - StatusBar, - SplashScreen, - CurrencyPipe, - HttpClient, - ClaService, - AuthService, - LfxHeaderService, - KeycloakService, - KEYCLOAK_HTTP_PROVIDER, - RolesService, - { provide: ErrorHandler, useClass: IonicErrorHandler } - ] -}) -export class AppModule { } diff --git a/cla-frontend-contributor-console/src/ionic/app/app.scss b/cla-frontend-contributor-console/src/ionic/app/app.scss deleted file mode 100755 index 0c40e874e..000000000 --- a/cla-frontend-contributor-console/src/ionic/app/app.scss +++ /dev/null @@ -1,107 +0,0 @@ -// http://ionicframework.com/docs/v2/theming/ - -// App Global Sass -// -------------------------------------------------- -// Put style rules here that you want to apply globally. These -// styles are for the entire app and not just one component. -// Additionally, this file can be also used as an entry point -// to import other Sass files to be included in the output CSS. -// -// Shared Sass variables, which can be used to adjust Ionic's -// default Sass variables, belong in "theme/variables.scss". -// -// To declare rules for a specific mode, create a child rule -// for the .md, .ios, or .wp mode classes. The mode class is -// automatically applied to the element in the app. - -//@import url('https://fonts.googleapis.com/css?family=Lato'); -//@import url('https://fonts.googleapis.com/css?family=Open+Sans'); -//@import url('https://fonts.googleapis.com/css?family=Work+Sans'); -@import "../assets/fonts/lato-font.scss"; -@import "../assets/fonts/open-sans-font.scss"; -@import "../theme/styles.scss"; - -body ion-app.ios { - font-family: "Lato", sans-serif !important; // font-family: -apple-system, "Helvetica Neue", "Roboto", sans-serif; -} - -body ion-app.md { - font-family: "Lato", sans-serif !important; -} - -body ion-app.wp { - font-family: "Lato", sans-serif !important; -} - -.grid { - padding-left: 0; - padding-right: 0; -} - -ion-icon { - color: color($colors, gray); -} - -ion-icon[name="menu"] { - color: color($colors, white); -} - -.item.item-md { - padding-left: 0; - .checkbox-md { - margin: 6px 15px 9px 0px; - } -} - -.item-md.item-block .item-inner { - padding-right: 0; -} - -.card-md .item-md.item-block .item-inner { - border-bottom: 1px solid #dedede; - padding-right: 0 !important; -} - -.toolbar-title-md { - padding: 0; -} - -.md ion-footer .toolbar:last-child { - padding-bottom: 1rem; -} - -.bar-buttons-md[end] { - margin-right: 0; - .button:last-child { - margin-right: 0; - } -} - -.row .col .card { - min-height: calc(100% - 20px); -} - -.content-top-show { - .scroll-content { - overflow-y: auto; - margin-top: 162px; - } -} -.content-top-hide { - .scroll-content { - overflow-y: auto; - margin-top: 102px; - } -} -// set footer + header height to keep footer at bottom; -// $headerAndFooterCollapseHeight: 200px; -// $headerAndFooterExpandHeight: 280px; - -.page-content { - transition: margin 0.3s; - min-height: calc(100vh - 247px); - - &.expanded { - min-height: calc(100vh - 306px); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/app/main.ts b/cla-frontend-contributor-console/src/ionic/app/main.ts deleted file mode 100755 index b02329c17..000000000 --- a/cla-frontend-contributor-console/src/ionic/app/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { AppModule } from './app.module'; -import { enableProdMode } from '@angular/core'; - -enableProdMode(); -platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/cla-frontend-contributor-console/src/ionic/assets/fonts/Lato/Lato-Regular.ttf b/cla-frontend-contributor-console/src/ionic/assets/fonts/Lato/Lato-Regular.ttf deleted file mode 100644 index 33eba8b19..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/fonts/Lato/Lato-Regular.ttf and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/fonts/Open_Sans/LICENSE.txt b/cla-frontend-contributor-console/src/ionic/assets/fonts/Open_Sans/LICENSE.txt deleted file mode 100644 index 75b52484e..000000000 --- a/cla-frontend-contributor-console/src/ionic/assets/fonts/Open_Sans/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/cla-frontend-contributor-console/src/ionic/assets/fonts/Open_Sans/OpenSans-Regular.ttf b/cla-frontend-contributor-console/src/ionic/assets/fonts/Open_Sans/OpenSans-Regular.ttf deleted file mode 100644 index 29bfd35a2..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/fonts/Open_Sans/OpenSans-Regular.ttf and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/fonts/lato-font.scss b/cla-frontend-contributor-console/src/ionic/assets/fonts/lato-font.scss deleted file mode 100644 index 66f7a9118..000000000 --- a/cla-frontend-contributor-console/src/ionic/assets/fonts/lato-font.scss +++ /dev/null @@ -1,5 +0,0 @@ -@font-face { - font-family: "Lato"; - src: url('../../assets/fonts/Lato/Lato-Regular.ttf') format("truetype"); - font-weight: 300; -} diff --git a/cla-frontend-contributor-console/src/ionic/assets/fonts/open-sans-font.scss b/cla-frontend-contributor-console/src/ionic/assets/fonts/open-sans-font.scss deleted file mode 100644 index 8268beb80..000000000 --- a/cla-frontend-contributor-console/src/ionic/assets/fonts/open-sans-font.scss +++ /dev/null @@ -1,5 +0,0 @@ -@font-face { - font-family: "sans-serif"; - src: url('../../assets/fonts/Open_Sans/OpenSans-Regular.ttf') format("truetype"); - font-weight: 300; -} diff --git a/cla-frontend-contributor-console/src/ionic/assets/icon/favicon.png b/cla-frontend-contributor-console/src/ionic/assets/icon/favicon.png deleted file mode 100755 index 5c41fc05d..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/icon/favicon.png and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/icon/logo.svg b/cla-frontend-contributor-console/src/ionic/assets/icon/logo.svg deleted file mode 100755 index bb07e6175..000000000 --- a/cla-frontend-contributor-console/src/ionic/assets/icon/logo.svg +++ /dev/null @@ -1 +0,0 @@ -LF Logo \ No newline at end of file diff --git a/cla-frontend-contributor-console/src/ionic/assets/img/boat.jpeg b/cla-frontend-contributor-console/src/ionic/assets/img/boat.jpeg deleted file mode 100644 index 7a64d37f3..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/img/boat.jpeg and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/img/cla-icon-logo.png b/cla-frontend-contributor-console/src/ionic/assets/img/cla-icon-logo.png deleted file mode 100644 index e40e5904f..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/img/cla-icon-logo.png and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/img/gray.png b/cla-frontend-contributor-console/src/ionic/assets/img/gray.png deleted file mode 100644 index 91945bc3e..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/img/gray.png and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/img/lf_logo.png b/cla-frontend-contributor-console/src/ionic/assets/img/lf_logo.png deleted file mode 100644 index b3ef2503b..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/img/lf_logo.png and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/assets/logo/lfx-easycla.png b/cla-frontend-contributor-console/src/ionic/assets/logo/lfx-easycla.png deleted file mode 100644 index 2e226188d..000000000 Binary files a/cla-frontend-contributor-console/src/ionic/assets/logo/lfx-easycla.png and /dev/null differ diff --git a/cla-frontend-contributor-console/src/ionic/claenv.d.ts b/cla-frontend-contributor-console/src/ionic/claenv.d.ts deleted file mode 100644 index 128b22c6c..000000000 --- a/cla-frontend-contributor-console/src/ionic/claenv.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -declare module '*.json' { - const value: any; - export default value; -} diff --git a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.html b/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.html deleted file mode 100644 index 1eb2ffb75..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.html +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.module.ts b/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.module.ts deleted file mode 100644 index 867e3dd9d..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicModule } from 'ionic-angular'; -import { GetHelpComponent } from './get-help'; - -@NgModule({ - declarations: [GetHelpComponent], - imports: [IonicModule], - exports: [GetHelpComponent] -}) -export class GetHelpComponentModule {} diff --git a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.scss b/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.scss deleted file mode 100644 index 5cf5769f3..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.scss +++ /dev/null @@ -1,21 +0,0 @@ -.sub-header { - position: absolute; - padding: 0.8rem 1rem; - right: 0; - background-color: #4c79b6; - width: 100%; -} - -button.help-btn { - background-color: #ffa400; - padding: 8px 16px; - color: #ffffff; - font-size: 12px; - border-radius: 200px; - cursor: pointer; - - a { - text-decoration: none; - color: white; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.ts b/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.ts deleted file mode 100644 index 8a0a02224..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/get-help/get-help.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'get-help', - templateUrl: 'get-help.html' -}) -export class GetHelpComponent { - - constructor() { - } - -} diff --git a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.html b/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.html deleted file mode 100644 index fd936e222..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.html +++ /dev/null @@ -1,3 +0,0 @@ -
      - -
      diff --git a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.module.ts b/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.module.ts deleted file mode 100644 index 6d2f929eb..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicModule } from 'ionic-angular'; -import { LoadingSpinnerComponent } from './loading-spinner'; - -@NgModule({ - declarations: [LoadingSpinnerComponent], - imports: [IonicModule], - exports: [LoadingSpinnerComponent] -}) -export class LoadingSpinnerComponentModule {} diff --git a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.scss b/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.scss deleted file mode 100644 index 88a396898..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.scss +++ /dev/null @@ -1,8 +0,0 @@ -loading-spinner { - .container { - position: relative; - margin: 1.5rem auto; - width: 28px; - height: 28px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.ts b/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.ts deleted file mode 100644 index 9052aadb4..000000000 --- a/cla-frontend-contributor-console/src/ionic/components/loading-spinner/loading-spinner.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Input, Component } from '@angular/core'; - -@Component({ - selector: 'loading-spinner', - templateUrl: 'loading-spinner.html' -}) -export class LoadingSpinnerComponent { - @Input('loading') - private loading: boolean; - - constructor() { - this.loading = true; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/constants/general.ts b/cla-frontend-contributor-console/src/ionic/constants/general.ts deleted file mode 100644 index 8592e0008..000000000 --- a/cla-frontend-contributor-console/src/ionic/constants/general.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const generalConstants = { - // Footer constants. - getHelpURL: 'https://jira.linuxfoundation.org/plugins/servlet/theme/portal/4/create/143', - acceptableUsePolicyURL: 'https://communitybridge.dev.platform.linuxfoundation.org/acceptable-use', - serviceSpecificTermsURL: 'https://communitybridge.dev.platform.linuxfoundation.org/service-terms', - platformUseAgreementURL: 'https://communitybridge.dev.platform.linuxfoundation.org/platform-use-agreement', - privacyPolicyURL: 'https://www.linuxfoundation.org/privacy/', - // End footer constants - linuxFoundationIdentityURL: 'https://identity.linuxfoundation.org/', - createTicketURL: 'https://jira.linuxfoundation.org/servicedesk/customer/portal/4', - easyCLAHelpURL: 'https://lf-docs-linux-foundation.gitbook.io/easycla/getting-started/easycla-faqs', - githubEmailURL: 'https://github.com/settings/emails', - easyCLADocURL: 'https://docs.linuxfoundation.org/lfx/easycla', - USER_MODEL: 'userModel', - PROJECT_MODEL: 'projectModel' - -} diff --git a/cla-frontend-contributor-console/src/ionic/declarations.d.ts b/cla-frontend-contributor-console/src/ionic/declarations.d.ts deleted file mode 100755 index 816172117..000000000 --- a/cla-frontend-contributor-console/src/ionic/declarations.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -/* - Declaration files are how the Typescript compiler knows about the type information(or shape) of an object. - They're what make intellisense work and make Typescript know all about your code. - - A wildcard module is declared below to allow third party libraries to be used in an app even if they don't - provide their own type declarations. - - To learn more about using third party libraries in an Ionic app, check out the docs here: - http://ionicframework.com/docs/v2/resources/third-party-libs/ - - For more info on type definition files, check out the Typescript docs here: - https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html -*/ -declare module '*'; diff --git a/cla-frontend-contributor-console/src/ionic/decorators/restricted.ts b/cla-frontend-contributor-console/src/ionic/decorators/restricted.ts deleted file mode 100644 index 208754ee3..000000000 --- a/cla-frontend-contributor-console/src/ionic/decorators/restricted.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -export function Restricted(restrictions: any) { - return function (target: Function) { - target.prototype.ionViewCanEnter = function () { - return true; - }; - }; -} diff --git a/cla-frontend-contributor-console/src/ionic/directives/loading-display/loading-display.module.ts b/cla-frontend-contributor-console/src/ionic/directives/loading-display/loading-display.module.ts deleted file mode 100644 index 632aa6cfc..000000000 --- a/cla-frontend-contributor-console/src/ionic/directives/loading-display/loading-display.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { LoadingDisplayDirective } from './loading-display'; -@NgModule({ - declarations: [LoadingDisplayDirective], - exports: [LoadingDisplayDirective] -}) -export class LoadingDisplayDirectiveModule {} diff --git a/cla-frontend-contributor-console/src/ionic/directives/loading-display/loading-display.ts b/cla-frontend-contributor-console/src/ionic/directives/loading-display/loading-display.ts deleted file mode 100644 index 38dbb5d3a..000000000 --- a/cla-frontend-contributor-console/src/ionic/directives/loading-display/loading-display.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Directive, ElementRef, Renderer2, Input, OnChanges, SimpleChange } from '@angular/core'; - -/** - * Generated class for the LoadingDisplayDirective directive. - * - * See https://angular.io/docs/ts/latest/api/core/index/DirectiveMetadata-class.html - * for more info on Angular Directives. - */ -@Directive({ - selector: '[loading-display]' // Attribute selector -}) -export class LoadingDisplayDirective implements OnChanges { - @Input('loading-display') loadingDisplay: any; - - constructor(public element: ElementRef, public renderer: Renderer2) {} - ngOnInit() { - this.renderer.addClass(this.element.nativeElement, 'loading-display-initial'); - } - - ngOnChanges(changes: { [propertyName: string]: SimpleChange }) { - if (changes['loadingDisplay'] && !this.loadingDisplay) { - this.renderer.addClass(this.element.nativeElement, 'loading-display-loaded'); - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/index.html b/cla-frontend-contributor-console/src/ionic/index.html deleted file mode 100755 index 7598ff174..000000000 --- a/cla-frontend-contributor-console/src/ionic/index.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - EasyCLA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.html b/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.scss b/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.ts b/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.ts deleted file mode 100644 index ac768eca3..000000000 --- a/cla-frontend-contributor-console/src/ionic/layout/cla-footer/cla-footer.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { ClaService } from '../../services/cla.service'; -import { generalConstants } from '../../constants/general'; - -@Component({ - selector: 'lfx-footer', - templateUrl: 'cla-footer.html' -}) -export class ClaFooter { - version: any; - releaseDate: any; - helpURL: string = generalConstants.getHelpURL; - acceptableUsePolicyURL: string = generalConstants.acceptableUsePolicyURL; - serviceSpecificTermsURL: string = generalConstants.serviceSpecificTermsURL; - platformUseAgreementURL: string = generalConstants.platformUseAgreementURL; - privacyPolicyURL: string = generalConstants.privacyPolicyURL; - documentationURL: string = generalConstants.easyCLADocURL; - constructor( - public claService: ClaService, - ) { - this.getReleaseVersion(); - } - - getReleaseVersion() { - this.claService.getReleaseVersion().subscribe((data) => { - this.version = data.version; - this.releaseDate = data.buildDate; - }) - } -} diff --git a/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.html b/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.html deleted file mode 100644 index 109f2c997..000000000 --- a/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - {{title}} - - - - - - - {{title}} - - diff --git a/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.scss b/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.scss deleted file mode 100644 index cb8efa20a..000000000 --- a/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.scss +++ /dev/null @@ -1,39 +0,0 @@ -.cla-header { - margin-top: 50px; - transition: margin 0.3s; - &.expanded { - margin-top: 105px; - } - - &.without-lfx-header { - margin-top: 0; - } - - .toolbar { - color: #7b7b7b; - background-color: white; - padding: 4px 25px; - height: 65px; - - &.with-lfx-header { - height: 55px; - } - - ion-icon { - color: #7b7b7b; - line-height: 6px; - - &:before { - font-size: 19px; - } - } - } - - .navbar-logo { - position: absolute; - left: calc(50% - 20px); - margin: -30px 0 -18px -138px; - width: 300px; - padding: 1rem; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.ts b/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.ts deleted file mode 100644 index cea0a74d5..000000000 --- a/cla-frontend-contributor-console/src/ionic/layout/cla-header/cla-header.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Events, NavController } from 'ionic-angular'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@Component({ - selector: 'cla-header', - templateUrl: 'cla-header.html' -}) -export class ClaHeader { - @Input() title = ''; - @Input() hasShowBackBtn = false; - @Output() onToggle: EventEmitter = new EventEmitter(); - hasExpanded: boolean = true; - - constructor( - public navCtrl: NavController, - ) { } - - onToggled() { - this.hasExpanded = !this.hasExpanded; - this.onToggle.emit(this.hasExpanded); - } - - - backToProjects() { - this.navCtrl.pop(); - } - -} - diff --git a/cla-frontend-contributor-console/src/ionic/layout/layout.module.ts b/cla-frontend-contributor-console/src/ionic/layout/layout.module.ts deleted file mode 100644 index 74ca22dfc..000000000 --- a/cla-frontend-contributor-console/src/ionic/layout/layout.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { ClaFooter } from './cla-footer/cla-footer'; -import { IonicModule } from 'ionic-angular'; -import { ClaHeader } from './cla-header/cla-header'; -import { lfxHeader } from './lfx-header/lfx-header'; -import { GetHelpComponentModule } from '../components/get-help/get-help.module'; - -@NgModule({ - declarations: [ClaFooter, ClaHeader, lfxHeader], - imports: [IonicModule, GetHelpComponentModule], - exports: [ClaFooter, ClaHeader] -}) -export class LayoutModule { } diff --git a/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.html b/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.scss b/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.ts b/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.ts deleted file mode 100644 index 1efab8a34..000000000 --- a/cla-frontend-contributor-console/src/ionic/layout/lfx-header/lfx-header.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'lfx-header', - templateUrl: 'lfx-header.html' -}) - -export class lfxHeader { - @Input() expanded; - @Output() toggled: EventEmitter = new EventEmitter(); -} diff --git a/cla-frontend-contributor-console/src/ionic/manifest.json b/cla-frontend-contributor-console/src/ionic/manifest.json deleted file mode 100755 index 6165b5ef6..000000000 --- a/cla-frontend-contributor-console/src/ionic/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "cla-console-app", - "short_name": "cla-console-app", - "start_url": "index.html", - "display": "standalone", - "icons": [{ - "src": "assets/icon/favicon.png", - "sizes": "512x512", - "type": "image/png" - }], - "background_color": "#4e8ef7", - "theme_color": "#4e8ef7" -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.html deleted file mode 100644 index c58ab7455..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - Request Access - - - - - - - - -
      - - - -

      {{serverError}}

      -
      -
      - - -

      Please send an E-Mail to your CLA's manager to set up a Corporate CLA account and add you to their - approved list.

      -
      -
      - - - Company Name*: - - -

      Enter your company name.

      - -

      A valid name is required - at least 3 characters are expected.

      -
      -
      - - - - CLA Manager Name*: - - -

      Enter your CLA manager's name.

      - -

      A valid name is required - at least 3 characters are expected.

      -
      -
      - - - CLA Manager E-Mail*: - - -

      Enter CLA Manager E-Mail.

      - -

      A valid email address is required.

      -
      -
      - - - Your E-mail* - - {{ useremail }} - - -

      Select the E-Mail address associated with this company.

      - -

      A valid email address is required.

      -
      -
      - - - Your Name*: - - -

      Enter your name.

      - -

      A valid name is required - at least 3 characters are expected.

      -
      -
      -
      -
      -
      -
      - - - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.module.ts deleted file mode 100644 index 7c24563f3..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaCompanyAdminSendEmailModal } from './cla-company-admin-send-email-modal'; - -@NgModule({ - declarations: [ClaCompanyAdminSendEmailModal], - imports: [IonicPageModule.forChild(ClaCompanyAdminSendEmailModal)], - entryComponents: [ClaCompanyAdminSendEmailModal] -}) -export class ClaCompanyAdminSendEmailModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.scss deleted file mode 100644 index b822565ab..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.scss +++ /dev/null @@ -1,26 +0,0 @@ -cla-company-admin-send-email-modal { - .field-notice { - font-size: 1.2rem; - color: #999; - text-align: right; - } - .toolbar-title .member-company { - font-weight: normal; - } - - .error { - color: red; - font-size: 12px; - } - - .input-forward-button { - margin-top: 2.6rem; - } - - .field-description { - margin-top: 0.3rem; - padding-left: 16px; - font-size: 1.2rem; - color: #999; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.ts deleted file mode 100644 index ddf0f4c40..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-send-email-modal/cla-company-admin-send-email-modal.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component, ViewChild } from '@angular/core'; -import { AlertController, IonicPage, ModalController, NavParams, ViewController } from 'ionic-angular'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { EmailValidator } from '../../validators/email'; -import { ClaService } from '../../services/cla.service'; -import { Content } from 'ionic-angular'; -import { generalConstants } from '../../constants/general'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId/invite-company-admin' -}) -@Component({ - selector: 'cla-company-admin-send-email-modal', - templateUrl: 'cla-company-admin-send-email-modal.html' -}) -export class ClaCompanyAdminSendEmailModal { - projectId: string; - companyId: string; - companyName: string; - userId: string; - authenticated: boolean; // true if coming from gerrit/corporate - userEmails: Array; - form: FormGroup; - serverError: string = ''; - isSendClicked = false; - @ViewChild('pageTop') pageTop: Content; - - constructor( - public navParams: NavParams, - public modalCtrl: ModalController, - public viewCtrl: ViewController, - public alertCtrl: AlertController, - private formBuilder: FormBuilder, - private claService: ClaService - ) { - this.userEmails = []; - this.projectId = navParams.get('projectId'); - // May be empty - this.companyId = navParams.get('companyId'); - // May be empty - this.companyName = navParams.get('companyName'); - this.userId = navParams.get('userId'); - this.authenticated = navParams.get('authenticated'); - this.form = formBuilder.group({ - company_name: ['', Validators.compose([Validators.required, Validators.minLength(3)])], - contributor_name: ['', Validators.compose([Validators.required, Validators.minLength(3)])], - contributor_email: ['', Validators.compose([Validators.required, EmailValidator.isValid])], - cla_manager_name: ['', Validators.compose([Validators.required, Validators.minLength(3)])], - cla_manager_email: ['', Validators.compose([Validators.required, EmailValidator.isValid])], - }); - } - - ngOnInit() { - this.getUserEmails(); - } - - getUserEmails() { - const user = JSON.parse(localStorage.getItem(generalConstants.USER_MODEL)); - if (user) { - this.userEmails = user.user_emails || []; - if (user.lf_email && this.userEmails.indexOf(user.lf_email) == -1) { - this.userEmails.push(user.lf_email); - } - } else { - console.warn('Unable to retrieve user.'); - } - } - - dismiss() { - this.viewCtrl.dismiss(); - } - - emailSent() { - let alert = this.alertCtrl.create({ - title: 'E-Mail Sent!', - subTitle: 'An E-Mail has been sent. Please wait for your CLA Manager to add you to your company approved list.', - buttons: ['Dismiss'] - }); - alert.present(); - } - - submit() { - this.isSendClicked = true; - if (this.form.valid) { - this.claService.getProject(this.projectId).subscribe((response) => { - // Instead of creating a company we need to send email to CLA Manager. - this.sendRequest(response); - }); - } - } - - sendRequest(project) { - this.serverError = ''; - let data = { - contributorName: this.form.value.contributor_name, - contributorEmail: this.form.value.contributor_email, - claManagerName: this.form.value.cla_manager_name, - claManagerEmail: this.form.value.cla_manager_email, - projectName: project.project_name, - companyName: this.form.value.company_name, - version: 'v1' - }; - - this.claService.postEmailToCompanyAdmin(this.userId, data).subscribe( - (response) => { - this.isSendClicked = false; - this.emailSent(); - this.dismiss(); - }, - (exception) => { - this.isSendClicked = false; - const errorObj = JSON.parse(exception._body); - if (errorObj) { - this.serverError = errorObj.Message; - this.pageTop.scrollToTop(); - } - } - ); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.html deleted file mode 100644 index 947b23715..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Verify Your Permission of Access - - - - - - - - -
      -
      -

      - - Are You a CLA Manager? -

      - -

      - Can you manage CLAs on behalf of your company? Are you authorized to approve the people who contribute on behalf - of your company? -

      - - - -
      -
      -
      - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.module.ts deleted file mode 100644 index e11dba4af..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaCompanyAdminYesnoModal } from './cla-company-admin-yesno-modal'; - -@NgModule({ - declarations: [ClaCompanyAdminYesnoModal], - imports: [ - // ClipboardModule, - IonicPageModule.forChild(ClaCompanyAdminYesnoModal) - ], - entryComponents: [ClaCompanyAdminYesnoModal] -}) -export class ClaCompanyAdminYesnoModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.scss deleted file mode 100644 index 3943f4c48..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.scss +++ /dev/null @@ -1,25 +0,0 @@ -cla-company-admin-yesno-modal { - .content-container { - max-width: 500px; - margin: 0 auto; - } - - .clearfix { - padding-top: 1px; - &::after { - content: ''; - display: block; - clear: both; - } - ion-icon { - margin-right: 1rem; - margin-bottom: 1rem; - } - [icon-left] { - float: left; - } - [icon-large] { - font-size: 9rem; - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.ts deleted file mode 100644 index eb1627155..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-company-admin-yesno-modal/cla-company-admin-yesno-modal.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { NavParams, ViewController, ModalController, IonicPage } from 'ionic-angular'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId/admin-yesno' -}) -@Component({ - selector: 'cla-company-admin-yesno-modal', - templateUrl: 'cla-company-admin-yesno-modal.html' -}) -export class ClaCompanyAdminYesnoModal { - projectId: string; - companyId: string; - companyName: string; - userId: string; - authenticated: boolean; //true if coming from gerrit/corporate - consoleLink: string; - - constructor( - public navParams: NavParams, - public viewCtrl: ViewController, - public modalCtrl: ModalController - ) { - this.projectId = navParams.get('projectId'); - // May be empty - this.companyId = navParams.get('companyId') || ''; - // May be empty - this.companyName = navParams.get('companyName') || ''; - this.userId = navParams.get('userId'); - this.authenticated = navParams.get('authenticated'); - this.consoleLink = EnvConfig['corp-console-link']; - } - - dismiss() { - this.viewCtrl.dismiss(); - } - - openCompanyAdminConsoleLink() { - window.open(this.consoleLink, '_blank'); - } - - openCompanyAdminSendEmail() { - let modal = this.modalCtrl.create('ClaCompanyAdminSendEmailModal', { - projectId: this.projectId, - companyId: this.companyId, - companyName: this.companyName, - userId: this.userId, - authenticated: this.authenticated - }); - modal.present(); - this.dismiss(); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.html deleted file mode 100644 index d53c4e7c3..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - Request Company Access to {{trimCharacter(company.company_name, 20)}} - - - - - - - - - -
      - - - -

      After you click send you will have to wait for {{ company.company_name }} CLA Manager to add you to their - approved list before you can complete your employee contributor CLA process. -

      -
      -
      - - - - - - Please select how you want to contact CLA Manager* - - - Select a CLA Manager from a list of options - - - - Enter CLA Manager Directly - - - -

      * Selecting an option is required.

      -
      -
      - -
      -
      - - - {{managers.length > 1 ? 'Select ': ''}}CLA Manager for {{ company.company_name }} - - - - {{ manager.username }} / {{ manager.lfEmail }} - - - - - - - - - - Enter a CLA Manager's Name and Email - - - CLA Manager Name: - - -

      - Type your CLA manager's name. - - * CLA manager's name is required. - -

      - - - CLA Manager E-Mail: - - -

      - Type an E-Mail. - - * A valid email address is required. - -

      -
      -
      - - - - Enter Your Name - - - - Your Name: - - -

      Add your name to help identify you to your CLA Manager. - - * Name is required - -

      -
      -
      - - - {{userEmails.length > 1 ? 'Select ': ''}}Your Email to Authorize - - - - {{ email }} - - - -

      - Select the email address attached to your account that you would like your - company's CLA Manager to approve. - - * A valid email address is required - -

      - -
      -
      - - - - Enter Message - - - - Message: - - -

      Explain to your CLA Manager who you are and why you would like to contribute to - {{project.project_name}} as an - employee of {{ company.company_name}}. - - * Message is required - -

      -
      -
      -
      -
      -
      - - - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.module.ts deleted file mode 100644 index 25a4de425..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaEmployeeRequestAccessModal } from './cla-employee-request-access-modal'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; - -@NgModule({ - declarations: [ClaEmployeeRequestAccessModal], - imports: [LoadingSpinnerComponentModule, IonicPageModule.forChild(ClaEmployeeRequestAccessModal)], - entryComponents: [ClaEmployeeRequestAccessModal] -}) -export class ClaEmployeeRequestAccessModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.scss deleted file mode 100644 index 08c6f8674..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.scss +++ /dev/null @@ -1,32 +0,0 @@ -cla-employee-request-access-modal { - .field-notice { - font-size: 1.2rem; - color: #999; - text-align: right; - } - .toolbar-title .member-company { - font-weight: normal; - } - - .input-forward-button { - margin-top: 2.6rem; - } - - .field-description { - margin-top: 0.3rem; - padding-left: 16px; - font-size: 1.2rem; - color: #999; - } - - .company-title { - font-size: 18px; - .name { - color: #4c79b6; - } - } - - .error-message { - color: #ff0000; - } -} \ No newline at end of file diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.ts deleted file mode 100644 index 95d88ed2d..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-employee-request-access-modal/cla-employee-request-access-modal.ts +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { AlertController, IonicPage, ModalController, NavParams, ViewController } from 'ionic-angular'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { EmailValidator } from '../../validators/email'; -import { ClaService } from '../../services/cla.service'; -import { generalConstants } from '../../constants/general'; - -@IonicPage({ - segment: 'cla/project/:projectId/repository/:repositoryId/user/:userId/employee/company/contact' -}) -@Component({ - selector: 'cla-employee-request-access-modal', - templateUrl: 'cla-employee-request-access-modal.html' -}) -export class ClaEmployeeRequestAccessModal { - project: any; - projectId: string; - userId: string; - companyId: string; - company: any; - authenticated: boolean; - cclaSignature: any; - managers: any; - formErrors: any[]; - - userEmails: Array = []; - - form: FormGroup; - submitAttempt: boolean = false; - currentlySubmitting: boolean = false; - loading: any; - showManagerSelectOption: boolean; - showManagerEnterOption: boolean; - - constructor( - public navParams: NavParams, - public modalCtrl: ModalController, - public viewCtrl: ViewController, - public alertCtrl: AlertController, - private formBuilder: FormBuilder, - private claService: ClaService - ) { - this.getDefaults(); - this.loading = true; - this.project = {}; - this.company = {}; - - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - this.companyId = navParams.get('companyId'); - this.authenticated = navParams.get('authenticated'); - - this.form = formBuilder.group({ - user_email: ['', Validators.compose([Validators.required, EmailValidator.isValid])], - user_name: ['', Validators.compose([Validators.required, Validators.minLength(3)])], - message: ['', Validators.compose([Validators.required])], - recipient_name: [''], - recipient_email: [''], - manager: [''], - managerOptions: ['', Validators.compose([Validators.required])] - }); - this.managers = []; - this.formErrors = []; - } - - saveManagerOption() { - const option = this.form.value.managerOptions; - if (option === 'select manager') { - this.showManagerSelectOption = true; - this.showManagerEnterOption = false; - this.resetFormValues('recipient_name'); - this.resetFormValues('recipient_email'); - this.form.controls['recipient_name'].clearValidators(); - this.form.controls['recipient_email'].clearValidators(); - } else if (option === 'enter manager') { - this.showManagerSelectOption = false; - this.showManagerEnterOption = true; - if (this.managers.length > 1) { - this.resetFormValues('manager'); - } - this.form.controls['recipient_name'].setValidators(Validators.compose([Validators.required])); - this.form.controls['recipient_email'].setValidators(Validators.compose([Validators.required, EmailValidator.isValid])); - } - this.form.controls['recipient_name'].updateValueAndValidity(); - this.form.controls['recipient_email'].updateValueAndValidity(); - } - - resetFormValues(value) { - return this.form.controls[value].reset(); - } - - getCLAManagerDetails(managerId) { - return this.managers.find((manager) => { - return (manager.userID === managerId); - }); - } - - getDefaults() { - this.userEmails = []; - } - - ngOnInit() { - this.project = JSON.parse(localStorage.getItem(generalConstants.PROJECT_MODEL)); - this.getUserEmails(); - this.getCompany(); - this.getProjectSignatures(); - } - - getUserEmails() { - const user = JSON.parse(localStorage.getItem(generalConstants.USER_MODEL)); - if (user) { - // For Gerrit user user_emails is always null and only have a LF_email. - this.userEmails = user.user_emails || []; - - if (user.lf_email && this.userEmails.indexOf(user.lf_email) == -1) { - this.userEmails.push(user.lf_email); - } - - if (this.userEmails.length === 1) { - this.form.controls['user_email'].setValue(user.user_emails[0]); - } - } - } - - getCompany() { - this.claService.getCompany(this.companyId).subscribe((response) => { - this.company = response; - }); - } - - insertAndSortManagersList(manager) { - this.managers.push(manager); - this.managers.sort((first, second) => { - return first.username.toLowerCase() - second.username.toLowerCase(); - }); - } - - getProjectSignatures() { - // Get CCLA Company Signatures - should just be one - this.loading = true; - this.claService.getCompanyProjectSignatures(this.companyId, this.projectId).subscribe( - (response) => { - this.loading = false; - if (response.signatures) { - let cclaSignatures = response.signatures.filter((sig) => sig.signatureType === 'ccla'); - if (cclaSignatures.length) { - this.cclaSignature = cclaSignatures[0]; - if (this.cclaSignature.signatureACL != null) { - if (this.cclaSignature.signatureACL.length === 1) { - this.form.controls['manager'].setValue(this.cclaSignature.signatureACL[0].userID); - } - for (let manager of this.cclaSignature.signatureACL) { - this.insertAndSortManagersList({ - userID: manager.userID, - username: manager.username, - lfEmail: manager.lfEmail - }); - } - } - } - } - }, - (exception) => { - this.loading = false; - } - ); - } - - // ContactUpdateModal modal dismiss - dismiss() { - this.viewCtrl.dismiss(); - } - - submit() { - this.submitAttempt = true; - this.currentlySubmitting = true; - this.formErrors = []; - - if (!this.form.valid) { - this.getFormValidationErrors(); - this.currentlySubmitting = false; - return; - } - - let managerEmail = ''; - let managerUsername = ''; - - // 'select manager' or 'enter manager' - if (this.form.value.manager && this.form.value.managerOptions === 'select manager') { - managerEmail = this.getCLAManagerDetails(this.form.value.manager).lfEmail; - managerUsername = this.getCLAManagerDetails(this.form.value.manager).username; - } else { - managerEmail = this.form.value.recipient_email; - managerUsername = this.form.value.recipient_name; - } - - let data = { - contributorId: this.userId, - contributorName: this.form.value.user_name, - contributorEmail: this.form.value.user_email, - message: this.form.value.message, - recipientName: managerUsername, - recipientEmail: managerEmail, - }; - - this.claService.requestToBeOnCompanyApprovedList(this.userId, this.companyId, this.projectId, data) - .subscribe((response) => { - this.loading = true; - this.emailSent(); - }, (error) => { - this.loading = true; - this.emailSentError(error); - }); - } - - emailSent() { - this.loading = false; - let message = this.authenticated - ? "Thank you for contacting your company's administrators. Once the CLA is signed and you are authorized, please navigate to the Agreements tab in the Gerrit Settings page and restart the CLA signing process" - : "Thank you for contacting your company's administrators. Once the CLA is signed and you are authorized, you will have to complete the CLA process from your existing pull request."; - let alert = this.alertCtrl.create({ - title: 'E-Mail Successfully Sent!', - subTitle: message, - buttons: ['Dismiss'] - }); - alert.onDidDismiss(() => this.dismiss()); - alert.present(); - } - - emailSentError(error) { - this.loading = false; - let message = `The request already exists for you. Please ask the CLA Manager to log into the EasyCLA Corporate Console and authorize you using one of the available methods.`; - let alert = this.alertCtrl.create({ - title: 'Problem Sending Request', - subTitle: message, - buttons: ['Dismiss'] - }); - alert.onDidDismiss(() => this.dismiss()); - alert.present(); - } - - getFormValidationErrors() { - let message; - Object.keys(this.form.controls).forEach((key) => { - const controlErrors = this.form.get(key).errors; - if (controlErrors != null) { - Object.keys(controlErrors).forEach((keyError) => { - switch (key) { - case 'managerOptions': - message = `*Selecting an Option for Entering a CLA Manager is ${keyError}`; - break; - case 'user_name': - message = `*User Name Field is ${keyError}`; - break; - case 'user_email': - message = `*Email Authorize Field is ${keyError}`; - break; - case 'recipient_email': - message = `*Receipent Email Field is ${keyError}`; - break; - case 'message': - message = `*Message Field is ${keyError}`; - break; - - default: - message = `Check Fields for errors`; - } - this.formErrors.push({ - message - }); - }); - } - }); - } - - trimCharacter(text, length) { - return text.length > length ? text.substring(0, length) + '...' : text; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.html deleted file mode 100644 index cd1f3e004..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - Corporate CLA - - - - - - - - -
      - -
      - -

      Corporate CLA

      -

      - Directions on who signs this and what happens. - The link will direct user to CLA corporate console. -

      -
      -
      - - -
      -
      - - - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.module.ts deleted file mode 100644 index 33966d9a4..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaNewCompanyModal } from './cla-new-company-modal'; - -// import { ClipboardModule } from 'ngx-clipboard'; - -@NgModule({ - declarations: [ClaNewCompanyModal], - imports: [ - // ClipboardModule, - IonicPageModule.forChild(ClaNewCompanyModal) - ], - entryComponents: [ClaNewCompanyModal] -}) -export class ClaNewCompanyModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.scss deleted file mode 100644 index 783aea051..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.scss +++ /dev/null @@ -1,60 +0,0 @@ -cla-new-company-modal { - .content-container { - max-width: 500px; - margin: 0 auto; - } - - .clearfix { - padding-top: 1px; - &::after { - content: ''; - display: block; - clear: both; - } - ion-icon { - margin-right: 1rem; - margin-bottom: 1rem; - } - [icon-left] { - float: left; - } - [icon-large] { - font-size: 9rem; - } - } - - .simple-sharing-link-management { - .link-management-container { - .link-management-text-container { - background-color: #f1f1f1; - border: 1px solid #d8d8d8; - .link-management-text { - border-right: 1px solid #d8d8d8; - padding: 1rem 2rem; - .link-management-label { - } - } - .link-management-copy-link-button { - padding: 0; - overflow: hidden; - button { - margin: 0; - width: 100%; - } - } - } - .link-management-url-container { - border: 1px solid #d8d8d8; - border-top-width: 0; - padding: 1rem; - .link-management-url-input { - border: none; - width: 100%; - } - .link-management-url-text-area { - display: none; - } - } - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.ts deleted file mode 100644 index f65b569c1..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-new-company-modal/cla-new-company-modal.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { NavParams, ViewController, IonicPage } from 'ionic-angular'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@IonicPage({ - segment: 'cla/project/:projectId/new-company' -}) -@Component({ - selector: 'cla-new-company-modal', - templateUrl: 'cla-new-company-modal.html' -}) -export class ClaNewCompanyModal { - projectId: string; - repositoryId: string; - userId: string; - consoleLink: string; - - @ViewChild('textArea') textArea: ElementRef; - - constructor( - public navParams: NavParams, - public viewCtrl: ViewController - ) { - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - this.getDefaults(); - } - - getDefaults() { - this.consoleLink = EnvConfig['corp-console-link']; - } - - dismiss() { - this.viewCtrl.dismiss(); - } - - openConsoleLink() { - window.open(this.consoleLink, '_blank'); - } - - copyText() { - let copyTextarea = this.textArea.nativeElement; - copyTextarea.select(); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.html deleted file mode 100644 index f77aaeada..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - Next Step - - - - - - - - - - - -

      Determining Your Next Step

      -
      -
      -

      You need to sign an ICLA

      -

      {{ 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 are done! -

      -

      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.

      - -

      - You may close this browser tab now and return to your repository page. -

      -
      -
      -
      -
      -
      -
      diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.module.ts deleted file mode 100644 index fe1131778..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaNextStepModal } from './cla-next-step-modal'; - -@NgModule({ - declarations: [ClaNextStepModal], - imports: [IonicPageModule.forChild(ClaNextStepModal)], - entryComponents: [ClaNextStepModal] -}) -export class ClaNextStepModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.scss deleted file mode 100644 index 5665a9b16..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.scss +++ /dev/null @@ -1,14 +0,0 @@ -cla-next-step-modal { - .field-notice { - font-size: 1.2rem; - color: #999; - text-align: right; - } - .toolbar-title .member-company { - font-weight: normal; - } - - .input-forward-button { - margin-top: 2.6rem; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.ts deleted file mode 100644 index a5e33da37..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-next-step-modal/cla-next-step-modal.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { NavController, NavParams, ViewController, IonicPage } from 'ionic-angular'; -import { ClaService } from '../../services/cla.service'; - -@IonicPage({ - segment: 'cla-next-step-modal' -}) -@Component({ - selector: 'cla-next-step-modal', - templateUrl: 'cla-next-step-modal.html', - providers: [] -}) -export class ClaNextStepModal { - projectId: string; - userId: string; - project: any; - signature: any; - userIsDone: boolean; - loading: any; - signingType: string; // "Gerrit" / "Github" - - constructor( - public navCtrl: NavController, - public navParams: NavParams, - public viewCtrl: ViewController, - private claService: ClaService - ) { - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - this.project = navParams.get('project'); - this.signature = navParams.get('signature'); - this.signingType = navParams.get('signingType'); - this.getDefaults(); - } - - getDefaults() { - this.loading = { - icla: true - }; - } - - ngOnInit() { - let requiresIcla = this.project.project_ccla_requires_icla_signature; - if (!requiresIcla) { - this.userIsDone = true; - this.loading.icla = false; - } else { - this.claService.getLastIndividualSignature(this.userId, this.projectId).subscribe((response) => { - if (response === null) { - // User has no icla, they need one - this.userIsDone = false; - } else { - // get whether icla is up to date - if (response.requires_resigning) { - this.userIsDone = false; - } else { - this.userIsDone = true; - } - } - this.loading.icla = false; - }); - } - } - - dismiss() { - this.viewCtrl.dismiss(); - } - - openIclaPage() { - this.navCtrl.push('ClaIndividualPage', { - projectId: this.projectId, - userId: this.userId - }); - } - - openGerritIclaPage() { - this.claService.getProjectGerrits(this.projectId).subscribe((gerrits) => { - if (gerrits.length) { - // picking the first Gerrit Instance will suffice in supplying a Gerrit ID, - // since all Gerrit Instances in the response will be under the same CLA Group. - let gerrit = gerrits[0]; - this.navCtrl.push('ClaGerritIndividualPage', { - gerritId: gerrit.gerrit_id - }); - } - }); - } - - gotoRepo() { - window.open(this.signature.signature_return_url, '_self'); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.html deleted file mode 100644 index 3accec00e..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - Select Company - - - - - - - - - - - - -
      - - - Search: - - - - - -
      - - - - - - - - - - -
      - - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.module.ts deleted file mode 100644 index 5517c1d71..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaSelectCompanyModal } from './cla-select-company-modal'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; -import { LoadingDisplayDirectiveModule } from '../../directives/loading-display/loading-display.module'; - -@NgModule({ - declarations: [ClaSelectCompanyModal], - imports: [ - LoadingSpinnerComponentModule, - LoadingDisplayDirectiveModule, - IonicPageModule.forChild(ClaSelectCompanyModal) - ], - entryComponents: [ClaSelectCompanyModal] -}) -export class ClaSelectCompanyModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.scss deleted file mode 100644 index c712c5759..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.scss +++ /dev/null @@ -1,14 +0,0 @@ -cla-select-company-modal { - .field-notice { - font-size: 1.2rem; - color: #999; - text-align: right; - } - .toolbar-title .member-company { - font-weight: normal; - } - - .input-forward-button { - margin-top: 2.6rem; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.ts deleted file mode 100644 index 5eb083a39..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-select-company-modal/cla-select-company-modal.ts +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { IonicPage, ModalController, NavController, NavParams, ViewController } from 'ionic-angular'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { ClaService } from '../../services/cla.service'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId/employee/company' -}) -@Component({ - selector: 'cla-select-company-modal', - templateUrl: 'cla-select-company-modal.html', - providers: [] -}) -export class ClaSelectCompanyModal { - form: FormGroup; - loading: any; - projectId: string; - repositoryId: string; - userId: string; - selectCompanyModalActive: boolean = false; - authenticated: boolean; - signature: string; - companies: any; - companiesFiltered: any; - - constructor( - public navCtrl: NavController, - public navParams: NavParams, - public viewCtrl: ViewController, - private modalCtrl: ModalController, - public formBuilder: FormBuilder, - private claService: ClaService - ) { - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - this.authenticated = navParams.get('authenticated'); - this.getDefaults(); - this.form = formBuilder.group({ - search: ['', Validators.compose([Validators.required])] - }); - } - - getDefaults() { - this.loading = { - companies: true, - activateSpinner: false - }; - this.companies = []; - } - - ngOnInit() { - this.getCompanies(); - } - - dismiss() { - this.viewCtrl.dismiss(); - } - - getCompanies() { - this.loading.companies = true; - this.claService.getAllCompanies().subscribe((response) => { - this.loading.companies = false; - if (response) { - // Cleanup - Remove any companies that don't have a name - this.companies = response.filter((company) => { - return company.company_name && company.company_name.trim().length > 0; - }); - - // Reset our filtered search - this.form.value.search = ''; - this.companiesFiltered = this.companies; - } - }); - } - - openClaEmployeeCompanyConfirmPage(company) { - // set loading spinner to true when a company is selected - this.loading.activateSpinner = true; - if (this.selectCompanyModalActive) { - return false; - } - this.selectCompanyModalActive = true; - - let data = { - project_id: this.projectId, - company_id: company.company_id, - user_id: this.userId - }; - - this.claService.postCheckAndPreparedEmployeeSignature(data).subscribe((response) => { - /* - Before an employee begins the signing process, ensure that - 1. The given project, company, and user exists - 2. The company signatory has signed the CCLA for their company. - 3. The user is included as part of the approved list of the CCLA that the company signed. - the CLA service will throw an error if any of the above is false. - */ - this.loading.activateSpinner = false; - let errors = response.hasOwnProperty('errors'); - console.log(`errors: ${errors}`); - this.selectCompanyModalActive = false; - if (errors) { - if (response.errors.hasOwnProperty('missing_ccla')) { - // When the company does NOT have a CCLA with the project: {'errors': {'missing_ccla': 'Company does not have CCLA with this project'}} - console.log(`errors - missing_ccla: ${response}`); - //this.openClaSendClaManagerEmailModal(company); - this.openClaCompanyAdminYesnoModal(company); - } - - if (response.errors.hasOwnProperty('ccla_approval_list')) { - console.log(`errors - ccla_approval_list: ${response}`); - // When the user is not whitelisted with the company: return {'errors': {'ccla_approval_list': 'No user email authorized for this ccla'}} - this.openClaEmployeeCompanyTroubleshootPage(company); - return; - } - } else { - // No Errors, expect normal signature response - this.signature = response; - this.navCtrl.push('ClaEmployeeCompanyConfirmPage', { - projectId: this.projectId, - repositoryId: this.repositoryId, - userId: this.userId, - companyId: company.company_id, - signingType: 'Github' - }); - } - }); - } - - openClaSendClaManagerEmailModal(company) { - let modal = this.modalCtrl.create('ClaSendClaManagerEmailModal', { - projectId: this.projectId, - userId: this.userId, - companyId: company.company_id, - authenticated: this.authenticated - }); - modal.present(); - } - - openClaCompanyAdminYesnoModal(company) { - let modal = this.modalCtrl.create('ClaCompanyAdminYesnoModal', { - projectId: this.projectId, - companyId: company == null ? '' : company.company_id, - companyName: company == null ? '' : company.company_name, - userId: this.userId, - authenticated: false // Github users are not authenticated. - }); - modal.present(); - this.dismiss(); - } - - openClaEmployeeCompanyTroubleshootPage(company) { - this.navCtrl.push('ClaEmployeeCompanyTroubleshootPage', { - projectId: this.projectId, - repositoryId: this.repositoryId, - userId: this.userId, - companyId: company.company_id, - gitService: 'GitHub' - }); - } - - onSearch() { - const searchTerm = this.form.value.search; - if (searchTerm === '') { - this.companiesFiltered = this.companies; - } else { - this.companiesFiltered = this.companies.filter((a) => { - return a.company_name.toLowerCase().includes(searchTerm.toLowerCase()); - }); - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.html b/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.html deleted file mode 100644 index 5f28a18ac..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - {{trimCharacter(company.company_name,20)}} - has not signed a CCLA for - {{trimCharacter(project.project_name,10)}} - - - - - - - - -
      - - - -

      Please check the fields below for errors.

      -
      -
      - - -

      Your company {{ company.company_name }} has not signed a Corporate CLA yet. Would - you like to send an E-Mail Notification to the CLA Manager to sign the Corporate CLA?

      -
      - - CCLA Approval List request already exists for you. - -
      - - - - Enter Your Name - - - - Your Name: - - -

      Add your name to help identify you to your CLA Manager. - - * Name is required - -

      -
      -
      - - - - - Email to Authorize - - {{ email }} - - -

      Select the email address attached to your account that you would like your - company's CLA Manager to approve.

      - -

      * A valid email address is required.

      -
      -
      -
      -
      -
      -
      - - - - - - - - - diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.module.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.module.ts deleted file mode 100644 index 9a20c1da1..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaSendClaManagerEmailModal } from './cla-send-cla-manager-email-modal'; - -@NgModule({ - declarations: [ClaSendClaManagerEmailModal], - imports: [IonicPageModule.forChild(ClaSendClaManagerEmailModal)], - entryComponents: [ClaSendClaManagerEmailModal] -}) -export class ClaSendClaManagerEmailModalModalModule {} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.scss b/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.scss deleted file mode 100644 index b10ba483f..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.scss +++ /dev/null @@ -1,34 +0,0 @@ -cla-send-cla-manager-email-modal { - .field-notice { - font-size: 1.2rem; - color: #999; - text-align: right; - } - .toolbar-title .member-company { - font-weight: normal; - } - - .input-forward-button { - margin-top: 2.6rem; - } - - .field-description { - margin-top: 0.3rem; - padding-left: 16px; - font-size: 1.2rem; - color: #999; - } - - .name { - color: #4c79b6; - } - - .error { - color: red; - } - - .toolbar-title { - font-size: 15px !important; - font-weight: bold; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.ts b/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.ts deleted file mode 100644 index a5c0eb732..000000000 --- a/cla-frontend-contributor-console/src/ionic/modals/cla-send-cla-manager-email-modal/cla-send-cla-manager-email-modal.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { AlertController, IonicPage, NavParams, ViewController } from 'ionic-angular'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { EmailValidator } from '../../validators/email'; -import { ClaService } from '../../services/cla.service'; -import { generalConstants } from '../../constants/general'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId/employee/company/contact' -}) -@Component({ - selector: 'cla-send-cla-manager-email-modal', - templateUrl: 'cla-send-cla-manager-email-modal.html' -}) -export class ClaSendClaManagerEmailModal { - projectId: string; - userId: string; - companyId: string; - authenticated: boolean; - hasRequestError: boolean = false; - company: any; - project: any; - userEmails: Array; - form: FormGroup; - submitAttempt: boolean = false; - currentlySubmitting: boolean = false; - - constructor( - public navParams: NavParams, - public viewCtrl: ViewController, - public alertCtrl: AlertController, - private formBuilder: FormBuilder, - private claService: ClaService - ) { - this.getDefaults(); - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - this.companyId = navParams.get('companyId'); - this.authenticated = navParams.get('authenticated'); - this.form = formBuilder.group({ - email: ['', Validators.compose([Validators.required, EmailValidator.isValid])], - user_name: ['', Validators.compose([Validators.required, Validators.minLength(3)])], - message: [''] - }); - } - - getDefaults() { - this.userEmails = []; - this.company = { - company_name: '' - }; - this.project = { - project_name: '' - }; - } - - ngOnInit() { - this.project = JSON.parse(localStorage.getItem(generalConstants.PROJECT_MODEL)); - this.getUserEmails(); - this.getCompany(); - } - - getUserEmails() { - const user = JSON.parse(localStorage.getItem(generalConstants.USER_MODEL)); - if (user) { - this.userEmails = user.user_emails || []; - if (user.lf_email && this.userEmails.indexOf(user.lf_email) == -1) { - this.userEmails.push(user.lf_email); - } - } else { - console.error('Unable to retrieve user.'); - } - } - - getCompany() { - this.claService.getCompany(this.companyId).subscribe((response) => { - this.company = response; - }); - } - - dismiss() { - this.viewCtrl.dismiss(); - } - - submit() { - this.hasRequestError = false; - this.submitAttempt = true; - this.currentlySubmitting = true; - if (!this.form.valid) { - this.currentlySubmitting = false; - return; - } - - const data = { - userId: this.userId, - userName: this.form.value.user_name, - userEmail: this.form.value.email, - }; - - this.claService.postCCLAWhitelistRequest(this.companyId, this.projectId, data).subscribe( - () => { - this.emailSent(); - }, - (exception) => { - this.hasRequestError = true; - } - ); - } - - emailSent() { - let alert = this.alertCtrl.create({ - title: 'E-Mail Successfully Sent!', - subTitle: - 'Thank you for contacting your CLA Manager. Once you are authorized, you will have to complete the CLA process from your existing pull request.', - buttons: ['Dismiss'] - }); - alert.onDidDismiss(() => this.dismiss()); - alert.present(); - } - - trimCharacter(text, length) { - return text.length > length ? text.substring(0, length) + '...' : text; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.html b/cla-frontend-contributor-console/src/ionic/pages/auth/auth.html deleted file mode 100644 index 220ffc388..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - -
      -
      {{message}}
      - -
      \ No newline at end of file diff --git a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.module.ts b/cla-frontend-contributor-console/src/ionic/pages/auth/auth.module.ts deleted file mode 100644 index b9b2107f7..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { LayoutModule } from '../../layout/layout.module'; -import { AuthPage } from './auth'; - -@NgModule({ - declarations: [], - imports: [IonicPageModule.forChild(AuthPage), LayoutModule] -}) -export class AuthPageModule { } diff --git a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.scss b/cla-frontend-contributor-console/src/ionic/pages/auth/auth.scss deleted file mode 100644 index 858f6d458..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.scss +++ /dev/null @@ -1,38 +0,0 @@ -page-auth { - .loader { - margin: auto; - margin-top: 200px; - border: 6px solid #f3f3f3; - border-radius: 50%; - border-top: 6px solid #003764; - width: 100px; - height: 100px; - -webkit-animation: spin 2s linear infinite; /* Safari */ - animation: spin 2s linear infinite; - } - - .message { - margin-top: 200px; - text-align: center; - font-size: 18px; - } - - /* Safari */ - @-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - } - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.ts b/cla-frontend-contributor-console/src/ionic/pages/auth/auth.ts deleted file mode 100644 index 19f4d11fb..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/auth/auth.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { AfterViewInit, Component, OnInit } from '@angular/core'; -import { NavController } from 'ionic-angular'; -import { AuthService } from '../../services/auth.service'; -import { LfxHeaderService } from '../../services/lfx-header.service'; - -/** - * Generated class for the AuthPage page. - * - * See https://ionicframework.com/docs/components/#navigation for more info on - * Ionic pages and navigation. - */ - -@Component({ - selector: 'page-auth', - templateUrl: 'auth.html' -}) -export class AuthPage implements AfterViewInit { - projectId: string; - claType: string; - message: string; - - constructor( - public navCtrl: NavController, - public authService: AuthService, - private lfxHeaderService: LfxHeaderService - ) { - this.projectId = localStorage.getItem('projectId'); - this.claType = localStorage.getItem('gerritClaType'); - } - - ngAfterViewInit() { - this.authService.redirectRoot.subscribe((target) => { - this.redirect(target); - }); - - this.authService.userProfile$.subscribe(user => { - if (user !== undefined) { - if (user) { - this.lfxHeaderService.setUserInLFxHeader(); - if (this.claType == 'ICLA') { - window.history.replaceState(null, null, window.location.pathname); - this.navCtrl.setRoot('ClaGerritIndividualPage', { projectId: this.projectId }); - } else if (this.claType == 'CCLA') { - window.history.replaceState(null, null, window.location.pathname); - this.navCtrl.setRoot('ClaGerritCorporatePage', { projectId: this.projectId }); - } else { - setTimeout(() => { - this.message = 'Invalid URL. Please verify you follow the right steps.'; - }, 1500); - } - } else { - this.navCtrl.setRoot('LoginPage'); - } - } - }); - } - - redirect(target) { - this.lfxHeaderService.setUserInLFxHeader(); - if (this.claType == 'ICLA') { - window.history.replaceState(null, null, window.location.pathname); - this.navCtrl.setRoot('ClaGerritIndividualPage', { projectId: this.projectId }); - } else if (this.claType == 'CCLA') { - window.history.replaceState(null, null, window.location.pathname); - this.navCtrl.setRoot('ClaGerritCorporatePage', { projectId: this.projectId }); - } else { - window.open(`${window.location.origin}` + '#' + target, '_self'); - } - } - - onClickToggle(toggle) { - - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.html b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.html deleted file mode 100644 index b83dcc394..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.html +++ /dev/null @@ -1,55 +0,0 @@ - - - -
      - - - - {{ project.name }} Logo - - -
      {{ project.project_name }}
      -
      -
      -
      - - - - - - - - Confirmation of Association with {{ company.company_name }} - -
      - - -

      - I hereby confirm that I am still affiliated with the company: {{ company.company_name }}. -

      -
      - -
      - -

      * You must agree in order to submit this form.

      -
      - - - -

      An error occurred while confirming your association with {{ company.company_name }}. - Error is: {{ errorMessage }}. Please contact the EasyCLA Help Desk at: {{ helpDeskLink }}

      -
      -
      -
      -
      -
      - -
      -
      -
      - -
      diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.module.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.module.ts deleted file mode 100644 index 245e08860..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaEmployeeCompanyConfirmPage } from './cla-employee-company-confirm'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; -import { LayoutModule } from '../../layout/layout.module'; -@NgModule({ - declarations: [ClaEmployeeCompanyConfirmPage], - imports: [LoadingSpinnerComponentModule, IonicPageModule.forChild(ClaEmployeeCompanyConfirmPage), LayoutModule], - entryComponents: [ClaEmployeeCompanyConfirmPage] -}) -export class ClaEmployeeCompanyConfirmPageModule { } diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.scss b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.scss deleted file mode 100644 index dd5cd6c46..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.scss +++ /dev/null @@ -1,13 +0,0 @@ -cla-employee-company-confirm { - loading-spinner.submit { - .container { - display: inline-block; - vertical-align: middle; - margin: 0; - } - } - - .project-title { - font-size: 25px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.ts deleted file mode 100644 index 26f85d2c0..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-confirm/cla-employee-company-confirm.ts +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { NavController, NavParams, IonicPage, ModalController } from 'ionic-angular'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { CheckboxValidator } from '../../validators/checkbox'; -import { ClaService } from '../../services/cla.service'; -import { generalConstants } from '../../constants/general'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@IonicPage({ - segment: 'project/:projectId/user/:userId/employee/company/:companyId/confirm' -}) -@Component({ - selector: 'cla-employee-company-confirm', - templateUrl: 'cla-employee-company-confirm.html' -}) -export class ClaEmployeeCompanyConfirmPage { - projectId: string; - repositoryId: string; - userId: string; - companyId: string; - signingType: string; // used to differentiate Github/Gerrit Users - user: any; - project: any; - company: any; - signature: any; - - form: FormGroup; - submitAttempt: boolean = false; - currentlySubmitting: boolean = false; - errorMessage: string = null; - helpDeskLink: URL = new URL(generalConstants.getHelpURL); - expanded: boolean = true; - - constructor( - public navCtrl: NavController, - private modalCtrl: ModalController, - public navParams: NavParams, - private formBuilder: FormBuilder, - private claService: ClaService - ) { - this.projectId = navParams.get('projectId'); - this.repositoryId = navParams.get('repositoryId'); - this.userId = navParams.get('userId'); - this.companyId = navParams.get('companyId'); - this.signingType = navParams.get('signingType'); - - this.getDefaults(); - - this.form = formBuilder.group({ - agree: [false, Validators.compose([CheckboxValidator.isChecked])] - }); - } - - getDefaults() { - this.project = { - project_name: '' - }; - this.company = { - company_name: '' - }; - this.user = { - user_name: '' - }; - this.errorMessage = null; - this.currentlySubmitting = false; - } - - ngOnInit() { - this.user = JSON.parse(localStorage.getItem(generalConstants.USER_MODEL)); - this.project = JSON.parse(localStorage.getItem(generalConstants.PROJECT_MODEL)); - this.getCompany(this.companyId); - } - - getCompany(companyId) { - this.claService.getCompany(companyId).subscribe((response) => { - this.company = response; - }); - } - - submit() { - // Reset our status and error messages - this.submitAttempt = true; - this.errorMessage = null; - this.currentlySubmitting = true; - - if (!this.form.valid) { - this.currentlySubmitting = false; - return; - } - - let signatureRequest = { - project_id: this.projectId, - company_id: this.companyId, - user_id: this.userId, - return_url_type: this.signingType //"Gerrit" / "Github" - }; - this.claService.postEmployeeSignatureRequest(signatureRequest).subscribe((response) => { - this.currentlySubmitting = false; - - let errors = response.hasOwnProperty('errors'); - if (errors) { - this.errorMessage = response.errors; - - if (response.errors.hasOwnProperty('ccla_approval_list')) { - // When the user is not whitelisted with the company: return {'errors': {'ccla_approval_list': 'No user email authorized for this ccla'}} - this.openClaEmployeeCompanyTroubleshootPage(); - return; - } - - if (response.errors.hasOwnProperty('missing_ccla')) { - // When the company does NOT have a CCLA with the project: {'errors': {'missing_ccla': 'Company does not have CCLA with this project'}} - // The user shouldn't get here if they are using the console properly - return; - } - } else { - // No Errors, expect normal signature response - this.errorMessage = null; - this.signature = response; - this.openClaNextStepModal(); - } - }); - } - - openClaNextStepModal() { - let modal = this.modalCtrl.create('ClaNextStepModal', { - projectId: this.projectId, - userId: this.userId, - project: this.project, - signature: this.signature, - signingType: this.signingType - }); - modal.present(); - } - - openClaEmployeeCompanyTroubleshootPage() { - this.navCtrl.push('ClaEmployeeCompanyTroubleshootPage', { - projectId: this.projectId, - repositoryId: this.repositoryId, - userId: this.userId, - companyId: this.companyId - }); - } - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.html b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.html deleted file mode 100644 index b6eb53f2f..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - -
      -
      - - - - {{ project.project_name }} Logo - - -

      {{ company.company_name }}

      -

      Unfortunately, you are not yet approved by the Corporate CLA of {{ company.company_name }} - on {{ project.project_name }}.

      -
      -
      -
      -
      - - - - - - - - If you feel you are receiving this message in error, please try the following steps: - - -

      - {{ company.company_name }} has whitelisted the following GitHub Organization(s), and all public - members therein will be approved to contribute: -

      -
        -
      • {{ org }}
      • -
      - -

      - - 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. -

      -
      -
      -
      -
      -
      - - - - - Contact the CLA Manager to be approved under their signed Corporate CLA. - - -

      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.

      - -
      -
      -
      -
      -
      -
      -
      - -
      diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.module.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.module.ts deleted file mode 100644 index 9e8fb37fc..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaEmployeeCompanyTroubleshootPage } from './cla-employee-company-troubleshoot'; -import { LayoutModule } from '../../layout/layout.module'; - -@NgModule({ - declarations: [ClaEmployeeCompanyTroubleshootPage], - imports: [IonicPageModule.forChild(ClaEmployeeCompanyTroubleshootPage), LayoutModule], - entryComponents: [ClaEmployeeCompanyTroubleshootPage] -}) -export class ClaEmployeeCompanyTroubleshootPageModule { } diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.scss b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.scss deleted file mode 100644 index b01ebed9d..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.scss +++ /dev/null @@ -1,6 +0,0 @@ -cla-employee-company-troubleshoot { - - @media (min-width: 768px) and (max-width: 1200px) { - padding: 16px 50px 162px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.ts deleted file mode 100644 index 835759fcf..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-employee-company-troubleshoot/cla-employee-company-troubleshoot.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { IonicPage, ModalController, NavParams } from 'ionic-angular'; -import { ClaService } from '../../services/cla.service'; -import { ClaSignatureModel } from '../../../../../cla-frontend-corporate-console/src/ionic/models/cla-signature'; -import { generalConstants } from '../../constants/general'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId/employee/company/:companyId/troubleshoot' -}) -@Component({ - selector: 'cla-employee-company-troubleshoot', - templateUrl: 'cla-employee-company-troubleshoot.html' -}) -export class ClaEmployeeCompanyTroubleshootPage { - loading: any; - projectId: string; - repositoryId: string; - userId: string; - companyId: string; - authenticated: boolean; - cclaSignature: any; - project: any; - company: any; - gitService: string; - expanded: boolean = true; - - constructor( - private modalCtrl: ModalController, - public navParams: NavParams, - private claService: ClaService - ) { - this.getDefaults(); - this.projectId = navParams.get('projectId'); - this.repositoryId = navParams.get('repositoryId'); - this.userId = navParams.get('userId'); - this.companyId = navParams.get('companyId'); - this.gitService = navParams.get('gitService'); - this.authenticated = navParams.get('authenticated'); - } - - getDefaults() { - this.loading = {}; - this.project = { - project_name: '', - logoUrl: '' - }; - this.company = { - company_name: '' - }; - this.cclaSignature = new ClaSignatureModel(); - } - - ngOnInit() { - this.getProject(); - this.getCompany(); - this.getProjectSignatures(); - } - - getProject() { - this.project = JSON.parse(localStorage.getItem(generalConstants.PROJECT_MODEL)); - this.loading.projects = false; - } - - getCompany() { - this.loading.companies = true; - this.claService.getCompany(this.companyId).subscribe((response) => { - this.loading.companies = true; - this.company = response; - }); - } - - getProjectSignatures() { - // Get CCLA Company Signatures - should just be one - this.loading.signatures = true; - this.claService.getCompanyProjectSignatures(this.companyId, this.projectId).subscribe( - (response) => { - this.loading.signatures = false; - if (response.signatures) { - let cclaSignatures = response.signatures.filter((sig) => sig.signatureType === 'ccla'); - if (cclaSignatures.length) { - this.cclaSignature = cclaSignatures[0]; - // Sort the values - if (this.cclaSignature.githubOrgWhitelist) { - const sortedList: string[] = this.cclaSignature.githubOrgWhitelist.sort((a, b) => { - return a.trim().localeCompare(b.trim()); - }); - // Remove duplicates - set doesn't allow dups - this.cclaSignature.githubOrgWhitelist = Array.from(new Set(sortedList)); - } - } - } - }, - (exception) => { - this.loading.signatures = false; - } - ); - } - - openGitServiceEmailSettings() { - window.open(generalConstants.githubEmailURL, '_blank'); - } - - openClaEmployeeRequestAccessModal() { - let modal = this.modalCtrl.create('ClaEmployeeRequestAccessModal', { - projectId: this.projectId, - repositoryId: this.repositoryId, - userId: this.userId, - companyId: this.companyId, - authenticated: this.authenticated - }); - modal.present(); - } - - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.html b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.html deleted file mode 100644 index 84b67dd04..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
      - -
      {{errorMessage}}
      - - - - - -
      - - - - - - -
      {{ company.company_name }}
      -
      - - - - - - -
      - - - -
      diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.module.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.module.ts deleted file mode 100644 index 0a421b62a..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaGerritCorporatePage } from './cla-gerrit-corporate'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; -import { LoadingDisplayDirectiveModule } from '../../directives/loading-display/loading-display.module'; -import { LayoutModule } from '../../layout/layout.module'; - -@NgModule({ - declarations: [ClaGerritCorporatePage], - imports: [ - LoadingSpinnerComponentModule, - LoadingDisplayDirectiveModule, - IonicPageModule.forChild(ClaGerritCorporatePage), - LayoutModule - ], - entryComponents: [ClaGerritCorporatePage] -}) -export class ClaGerritCorporatePageModule { } diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.scss b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.scss deleted file mode 100644 index 874879bb1..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.scss +++ /dev/null @@ -1,28 +0,0 @@ -.page-content { - .error { - font-size: 18px; - color: red; - text-align: center; - padding-top: 15px; - } - - .search-box { - margin-left: 10px; - .header { - font-weight: bold; - } - } - - .table-view { - height: 54vh; - overflow-y: auto; - - td { - cursor: pointer; - } - } - - @media (min-width: 768px) and (max-width: 1200px) { - padding: 16px 50px 162px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.ts deleted file mode 100644 index 9ce1782c9..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-corporate/cla-gerrit-corporate.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { NavController, NavParams, ViewController, ModalController, IonicPage } from 'ionic-angular'; -import { ClaService } from '../../services/cla.service'; -import { AuthService } from '../../services/auth.service'; -import { Restricted } from '../../decorators/restricted'; -import { generalConstants } from '../../constants/general'; - -@Restricted({ - roles: ['isAuthenticated'] -}) -@IonicPage({ - segment: 'cla/gerrit/project/:projectId/corporate' -}) -@Component({ - selector: 'cla-gerrit-corporate', - templateUrl: 'cla-gerrit-corporate.html', - providers: [] -}) -export class ClaGerritCorporatePage { - loading: any; - projectId: string; - userId: string; - signature: string; - companies: any; - filteredCompanies: any; - expanded: boolean = true; - errorMessage: string = null; - searchTimer = null; - - constructor( - public navCtrl: NavController, - public navParams: NavParams, - public viewCtrl: ViewController, - private modalCtrl: ModalController, - private claService: ClaService, - private authService: AuthService, - ) { - this.projectId = navParams.get('projectId'); - this.getDefaults(); - localStorage.setItem('projectId', this.projectId); - localStorage.setItem('gerritClaType', 'CCLA'); - } - - getDefaults() { - this.loading = { - companies: true - }; - this.companies = []; - this.filteredCompanies = []; - } - - ngOnInit() { - this.authService.userProfile$.subscribe(user => { - if (user !== undefined) { - if (user) { - this.getProject(); - } else { - this.redirectToLogin(); - } - } - }); - } - - redirectToLogin() { - this.navCtrl.setRoot('LoginPage'); - } - - getCompanies() { - this.claService.getAllCompanies().subscribe((response) => { - if (response) { - this.companies = response; - this.filteredCompanies = this.companies; - } - this.loading.companies = false; - }); - } - - getUserInfo() { - // retrieve userInfo from auth0 service - this.claService.postOrGetUserForGerrit().subscribe((user) => { - localStorage.setItem(generalConstants.USER_MODEL, JSON.stringify(user)); - this.userId = user.user_id; - this.getCompanies(); - }, (error) => { - // Got an auth error, redirect to the login - this.loading = false; - setTimeout(() => this.redirectToLogin()); - }); - } - - openClaEmployeeCompanyConfirmPage(company) { - let data = { - project_id: this.projectId, - company_id: company.company_id, - user_id: this.userId - }; - this.claService.postCheckAndPreparedEmployeeSignature(data).subscribe((response) => { - let errors = response.hasOwnProperty('errors'); - if (errors) { - if (response.errors.hasOwnProperty('missing_ccla')) { - // When the company does NOT have a CCLA with the project: {'errors': {'missing_ccla': 'Company does not have CCLA with this project'}} - this.openClaSendClaManagerEmailModal(company); - } - - if (response.errors.hasOwnProperty('ccla_approval_list')) { - // When the user is not whitelisted with the company: return {'errors': {'ccla_approval_list': 'No user email authorized for this ccla'}} - this.openClaEmployeeCompanyTroubleshootPage(company); - return; - } - } else { - this.signature = response; - - this.navCtrl.push('ClaEmployeeCompanyConfirmPage', { - projectId: this.projectId, - signingType: 'Gerrit', - userId: this.userId, - companyId: company.company_id - }); - } - }); - } - - openClaSendClaManagerEmailModal(company) { - let modal = this.modalCtrl.create('ClaSendClaManagerEmailModal', { - projectId: this.projectId, - userId: this.userId, - companyId: company.company_id, - authenticated: true - }); - modal.present(); - } - - openClaNewCompanyModal() { - let modal = this.modalCtrl.create('ClaNewCompanyModal', { - projectId: this.projectId - }); - modal.present(); - } - - openClaCompanyAdminYesnoModal() { - let modal = this.modalCtrl.create('ClaCompanyAdminYesnoModal', { - projectId: this.projectId, - userId: this.userId, - authenticated: true - }); - modal.present(); - } - - openClaEmployeeCompanyTroubleshootPage(company) { - this.navCtrl.push('ClaEmployeeCompanyTroubleshootPage', { - projectId: this.projectId, - repositoryId: '', - userId: this.userId, - companyId: company.company_id, - gitService: 'Gerrit' - }); - } - - getProject() { - this.claService.getProjectWithAuthToken(this.projectId).subscribe( - (project) => { - this.errorMessage = ''; - localStorage.setItem(generalConstants.PROJECT_MODEL, JSON.stringify(project)); - this.getUserInfo(); - }, - () => { - this.loading = false; - this.errorMessage = 'Invalid project id.'; - } - ); - } - - onSearch(event) { - const searchText = event._value; - if (this.searchTimer !== null) { - clearTimeout(this.searchTimer); - } - this.searchTimer = setTimeout(() => { - if (searchText === '') { - this.filteredCompanies = this.companies; - } else { - this.filteredCompanies = this.companies.filter((a) => { - return a.company_name.toLowerCase().includes(searchText.toLowerCase()); - }); - } - }, 250); - } - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.html b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.html deleted file mode 100644 index 9b0e039f1..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.html +++ /dev/null @@ -1,56 +0,0 @@ - - - -
      - -
      {{errorMessage}}
      - - - -
      - - - -

      {{ project.project_name }}

      -
      -
      -
      -
      - - - - - - - - Individual CLA - -
      -

      - 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. -

      -
      -
      -
      -
      -
      -
      -
      - - - -
      \ No newline at end of file diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.module.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.module.ts deleted file mode 100644 index 3d9ab642c..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; -import { LayoutModule } from '../../layout/layout.module'; -import { ClaGerritIndividualPage } from './cla-gerrit-individual'; - -@NgModule({ - declarations: [ClaGerritIndividualPage], - imports: [IonicPageModule.forChild(ClaGerritIndividualPage), - LayoutModule, LoadingSpinnerComponentModule], - entryComponents: [ClaGerritIndividualPage] -}) -export class ClaGerritIndividualPageModule { } diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.scss b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.scss deleted file mode 100644 index 20d936410..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.scss +++ /dev/null @@ -1,15 +0,0 @@ -cla-gerrit-individual { - .error { - font-size: 18px; - color: red; - text-align: center; - padding-top: 15px; - } - .card-md p { - margin-bottom: 1rem; - } - - @media (min-width: 768px) and (max-width: 1200px) { - padding: 16px 50px 162px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.ts deleted file mode 100644 index c24a6b86c..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-gerrit-individual/cla-gerrit-individual.ts +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { NavController, NavParams, IonicPage } from 'ionic-angular'; -import { ClaService } from '../../services/cla.service'; -import { AuthService } from '../../services/auth.service'; -import { Restricted } from '../../decorators/restricted'; -import { generalConstants } from '../../constants/general'; - -@Restricted({ - roles: ['isAuthenticated'] -}) -@IonicPage({ - segment: 'cla/gerrit/project/:projectId/individual' -}) -@Component({ - selector: 'cla-gerrit-individual', - templateUrl: 'cla-gerrit-individual.html' -}) -export class ClaGerritIndividualPage { - projectId: string; - project: any; - gerrit: any; - userId: string; - user: any; - signatureIntent: any; - activeSignatures: boolean = true; // we assume true until otherwise - signature: any; - expanded: boolean = true; - errorMessage: string; - loading: boolean; - - constructor( - public navCtrl: NavController, - public navParams: NavParams, - private claService: ClaService, - private authService: AuthService, - ) { - this.getDefaults(); - this.projectId = navParams.get('projectId'); - localStorage.setItem('projectId', this.projectId); - localStorage.setItem('gerritClaType', 'ICLA'); - } - - getDefaults() { - this.project = { - project_name: '' - }; - this.signature = { - sign_url: '' - }; - } - - ngOnInit() { - this.loading = true; - this.authService.userProfile$.subscribe(user => { - if (user !== undefined) { - if (user) { - this.getProject(); - } else { - this.redirectToLogin(); - } - } - }); - } - - redirectToLogin() { - this.navCtrl.setRoot('LoginPage'); - } - - getProject() { - this.claService.getProjectWithAuthToken(this.projectId).subscribe( - (project) => { - this.project = project; - localStorage.setItem(generalConstants.PROJECT_MODEL, JSON.stringify(project)); - // retrieve userInfo from auth0 service - this.getUserDetails(); - }, - () => { - this.loading = false; - this.errorMessage = 'Invalid project id.'; - } - ); - } - - getUserDetails() { - this.claService.postOrGetUserForGerrit().subscribe( - (user) => { - this.userId = user.user_id; - localStorage.setItem(generalConstants.USER_MODEL, JSON.stringify(user)); - // get signatureIntent object, similar to the Github flow. - this.postSignatureRequest(); - }, - (exception) => { - this.loading = false; - this.errorMessage = 'Invalid user details, please login again.'; - } - ); - } - postSignatureRequest() { - let signatureRequest = { - project_id: this.projectId, - user_id: this.userId, - return_url_type: 'Gerrit' - }; - this.claService.postIndividualSignatureRequest(signatureRequest).subscribe( - (response) => { - this.loading = false; - this.signature = response; - }, - () => { - this.loading = false; - this.errorMessage = 'Invalid signature.'; - } - ); - } - - openClaAgreement() { - if (!this.signature.sign_url) { - return; - } - window.open(this.signature.sign_url, '_self'); - } - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.html b/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.html deleted file mode 100644 index 49bdba14a..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.html +++ /dev/null @@ -1,65 +0,0 @@ - - - -
      - - - - {{ project.name }} Logo - - -
      {{ project.project_name }}
      -
      -
      -
      - - - - - - - - Individual CLA - -
      -

      - 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. -

      -
      - -
      Loading Individual CLA...
      -
      -
      -

      - 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. -

      -
      -
      -
      -
      -
      -
      -
      - - -
      diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.module.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.module.ts deleted file mode 100644 index 72ff8d6fe..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaIndividualPage } from './cla-individual'; -import { LayoutModule } from '../../layout/layout.module'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; - -@NgModule({ - declarations: [ClaIndividualPage], - imports: [IonicPageModule.forChild(ClaIndividualPage), LayoutModule, LoadingSpinnerComponentModule], - entryComponents: [ClaIndividualPage] -}) -export class ClaIndividualPageModule { } diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.scss b/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.scss deleted file mode 100644 index ed8028f36..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.scss +++ /dev/null @@ -1,30 +0,0 @@ -cla-individual { - .error { - color: red; - } - .card-md p { - margin-bottom: 1rem; - } - - .project-title { - font-size: 25px; - } - - img { - background-size: contain; - max-height: 150px; - } - - .clear-div { - clear: both; - } - - .error { - color: #db4e4e; - font-size: 12px; - - a { - cursor: pointer; - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.ts deleted file mode 100644 index 4827c5c22..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-individual/cla-individual.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { NavParams, IonicPage } from 'ionic-angular'; -import { ClaService } from '../../services/cla.service'; -import { generalConstants } from '../../constants/general'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId/individual' -}) -@Component({ - selector: 'cla-individual', - templateUrl: 'cla-individual.html' -}) -export class ClaIndividualPage { - projectId: string; - userId: string; - user: any; - project: any; - signatureIntent: any; - activeSignatures: boolean = true; - signature: any; - loadingSignature: boolean = true; - error: any = false; - expanded: boolean = true; - - constructor( - public navParams: NavParams, - private claService: ClaService - ) { - this.getDefaults(); - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - } - - getDefaults() { - this.project = { - project_name: '' - }; - this.signature = { - sign_url: '' - }; - } - - ngOnInit() { - this.user = JSON.parse(localStorage.getItem(generalConstants.USER_MODEL)); - this.project = JSON.parse(localStorage.getItem(generalConstants.PROJECT_MODEL)); - this.getUserSignatureIntent(); - } - - getUserSignatureIntent() { - this.loadingSignature = true; - this.claService.getUserSignatureIntent(this.userId).subscribe((response) => { - this.signatureIntent = response; - if (this.signatureIntent !== null) { - this.postSignatureRequest(); - } else { - this.activeSignatures = false; - } - this.loadingSignature = false; - }); - } - - postSignatureRequest() { - let signatureRequest = { - project_id: this.projectId, - user_id: this.userId, - return_url_type: 'Github', - return_url: this.signatureIntent.return_url - }; - - this.claService.postIndividualSignatureRequest(signatureRequest).subscribe( - (response) => { - if (response.errors) { - this.error = response.errors; - } else { - this.signature = response; - } - }, - (err) => { - this.error = err; - } - ); - } - - createTicket() { - window.open(generalConstants.createTicketURL, '_blank'); - } - - openClaAgreement() { - if (!this.signature.sign_url) { - return; - } - window.open(this.signature.sign_url, '_self'); - } - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.html b/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.html deleted file mode 100755 index 0330a3275..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.html +++ /dev/null @@ -1,47 +0,0 @@ - - - -
      - - - - - {{ project.project_name }} Logo - - - - - -
      {{ project.project_name }}
      -
      - - - - - - - Corporate - -

      Select this if you are contributing code as an employee.

      -
      -
      -
      - - - - - - - Individual - -

      Select this if you are contributing code as an individual.

      -
      -
      -
      -
      -
      -
      -
      -
      - -
      diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.module.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.module.ts deleted file mode 100644 index b20eb0586..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { ClaLandingPage } from './cla-landing'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; -import { LoadingDisplayDirectiveModule } from '../../directives/loading-display/loading-display.module'; -import { LayoutModule } from '../../layout/layout.module'; - -@NgModule({ - declarations: [ClaLandingPage], - imports: [ - LoadingSpinnerComponentModule, - LoadingDisplayDirectiveModule, - IonicPageModule.forChild(ClaLandingPage), - LayoutModule - ], - entryComponents: [ClaLandingPage] -}) -export class ClaLandingPageModule {} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.scss b/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.scss deleted file mode 100755 index 07c369393..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.scss +++ /dev/null @@ -1,20 +0,0 @@ -cla-landing { - img { - max-height: 150px; - } - - .project-name { - text-align: center; - font-size: 25px; - } - - .xc { - display: flex; - align-items: center; - justify-content: center; - } - - @media (min-width: 768px) and (max-width: 1200px) { - padding: 16px 50px 100px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.ts b/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.ts deleted file mode 100755 index df54c6ce9..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/cla-landing/cla-landing.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { IonicPage, ModalController, NavController, NavParams } from 'ionic-angular'; -import { ClaService } from '../../services/cla.service'; -import { generalConstants } from '../../constants/general'; -import { EnvConfig } from '../../services/cla.env.utils'; - -@IonicPage({ - segment: 'cla/project/:projectId/user/:userId' -}) -@Component({ - selector: 'cla-landing', - templateUrl: 'cla-landing.html' -}) -export class ClaLandingPage { - projectId: string; - userId: string; - user: any; - project: any; - expanded: boolean = true; - - constructor( - public navCtrl: NavController, - public navParams: NavParams, - private modalCtrl: ModalController, - private claService: ClaService - ) { - this.projectId = navParams.get('projectId'); - this.userId = navParams.get('userId'); - this.getDefaults(); - } - - getDefaults() { - this.project = { - project_name: '' - }; - } - - ngOnInit() { - this.getUser(); - this.getProject(); - localStorage.removeItem('gerritClaType'); - } - - openClaIndividualPage() { - // send to the individual cla page which will give directions and redirect - this.navCtrl.push('ClaIndividualPage', { - projectId: this.projectId, - userId: this.userId - }); - } - - openClaIndividualEmployeeModal() { - let modal = this.modalCtrl.create('ClaSelectCompanyModal', { - projectId: this.projectId, - userId: this.userId - }); - modal.present(); - } - - getUser() { - this.claService.getUser(this.userId).subscribe((response) => { - localStorage.setItem(generalConstants.USER_MODEL, JSON.stringify(response)); - this.user = response; - }); - } - - getProject() { - this.claService.getProject(this.projectId).subscribe((response) => { - localStorage.setItem(generalConstants.PROJECT_MODEL, JSON.stringify(response)); - this.project = response; - }); - } - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/login/login.html b/cla-frontend-contributor-console/src/ionic/pages/login/login.html deleted file mode 100644 index 580a38761..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/login/login.html +++ /dev/null @@ -1,16 +0,0 @@ - - - -
      - -
      - - - -
      diff --git a/cla-frontend-contributor-console/src/ionic/pages/login/login.module.ts b/cla-frontend-contributor-console/src/ionic/pages/login/login.module.ts deleted file mode 100644 index 60b5eb4b2..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/login/login.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { LoginPage } from './login'; -import { LoadingSpinnerComponentModule } from '../../components/loading-spinner/loading-spinner.module'; -import { LoadingDisplayDirectiveModule } from '../../directives/loading-display/loading-display.module'; -import { LayoutModule } from '../../layout/layout.module'; - -@NgModule({ - declarations: [LoginPage], - imports: [ - LoadingSpinnerComponentModule, - LoadingDisplayDirectiveModule, - IonicPageModule.forChild(LoginPage), - LayoutModule - ], - entryComponents: [LoginPage] -}) -export class LoginPageModule {} diff --git a/cla-frontend-contributor-console/src/ionic/pages/login/login.scss b/cla-frontend-contributor-console/src/ionic/pages/login/login.scss deleted file mode 100644 index d28a65e41..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/login/login.scss +++ /dev/null @@ -1,46 +0,0 @@ -login { - .scroll-content { - background: url("../assets/img/boat.jpeg") no-repeat center center fixed #000; - -webkit-background-size: cover; - -moz-background-size: cover; - -o-background-size: cover; - background-size: cover; - padding: 0 !important; - } - - .page-content { - .login-container { - text-align: center; - margin-top: 215px; - margin-left: auto; - margin-right: auto; - padding: 50px 20px; - background-color: #ececec; - max-width: 400px; - - button { - margin-top: 25px; - width: 250px; - max-width: 100%; - } - } - - @media (min-width: 768px) and (max-width: 1200px) { - padding: 16px 50px 162px; - } - } - - .logo { - display: block; - width: 250px; - max-width: 100%; - height: auto; - margin-left: auto; - margin-right: auto; - margin-bottom: 25px; - } - - .scroll-content { - padding-bottom: 162px; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/pages/login/login.ts b/cla-frontend-contributor-console/src/ionic/pages/login/login.ts deleted file mode 100644 index 440d85051..000000000 --- a/cla-frontend-contributor-console/src/ionic/pages/login/login.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Component } from '@angular/core'; -import { IonicPage } from 'ionic-angular'; -import { AuthService } from '../../services/auth.service'; -import { AUTH_ROUTE } from '../../services/auth.utils'; - -@IonicPage({ - name: 'LoginPage', - segment: 'login' -}) -@Component({ - selector: 'login', - templateUrl: 'login.html' -}) -export class LoginPage { - canAccess: boolean; - expanded: boolean = true; - - constructor( - public authService: AuthService - ) { } - - login() { - this.authService.login(AUTH_ROUTE); - } - - onClickToggle(hasExpanded) { - this.expanded = hasExpanded; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/service-worker.js b/cla-frontend-contributor-console/src/ionic/service-worker.js deleted file mode 100755 index 10d474b7a..000000000 --- a/cla-frontend-contributor-console/src/ionic/service-worker.js +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -/** - * Check out https://googlechrome.github.io/sw-toolbox/docs/master/index.html for - * more info on how to use sw-toolbox to custom configure your service worker. - */ - -'use strict'; -importScripts('./build/sw-toolbox.js'); - -self.toolbox.options.cache = { - name: 'ionic-cache' -}; - -// pre-cache our key assets -self.toolbox.precache(['./build/main.js', './build/main.css', './build/polyfills.js', 'index.html', 'manifest.json']); - -// dynamically cache any other local assets -//self.toolbox.router.any('/*', self.toolbox.cacheFirst); -// disable cache for now: -self.toolbox.router.any('/*', self.toolbox.networkOnly); - -// for any other requests go to the network, cache, -// and then only use that cached resource if your user goes offline -//self.toolbox.router.default = self.toolbox.networkFirst; -// disable cache for now: -self.toolbox.router.default = self.toolbox.networkOnly; diff --git a/cla-frontend-contributor-console/src/ionic/services/auth.service.ts b/cla-frontend-contributor-console/src/ionic/services/auth.service.ts deleted file mode 100644 index 3dc62db46..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/auth.service.ts +++ /dev/null @@ -1,231 +0,0 @@ - -import { Injectable } from '@angular/core'; -import createAuth0Client from '@auth0/auth0-spa-js'; -import Auth0Client from '@auth0/auth0-spa-js/dist/typings/Auth0Client'; -import { - Observable, - BehaviorSubject, - Subject -} from 'rxjs'; -import { tap, catchError, concatMap, shareReplay } from 'rxjs/operators'; -import * as querystring from 'query-string'; -import Url from 'url-parse'; -import { from } from 'rxjs/observable/from'; -import { of } from 'rxjs/observable/of'; -import { reject } from 'lodash'; -import { combineLatest } from 'rxjs/observable/combineLatest'; -import { EnvConfig } from './cla.env.utils'; - -@Injectable() -export class AuthService { - auth0Options = { - clientId: EnvConfig['auth0-clientId'], - domain: EnvConfig['auth0-domain'], - }; - - currentHref = window.location.href; - redirectRoot: Subject = new Subject(); - loading$ = new BehaviorSubject(true); - // Create an observable of Auth0 instance of client - auth0Client$ = (from( - createAuth0Client({ - domain: this.auth0Options.domain, - client_id: this.auth0Options.clientId - }) - ) as Observable).pipe( - shareReplay(1), // Every subscription receives the same shared value - catchError((err) => { - this.loading$.next(false); - return reject(err); - }) - ); - // Define observables for SDK methods that return promises by default - // For each Auth0 SDK method, first ensure the client instance is ready - // concatMap: Using the client instance, call SDK method; SDK returns a promise - // from: Convert that resulting promise into an observable - isAuthenticated$ = this.auth0Client$.pipe( - concatMap((client: Auth0Client) => from(client.isAuthenticated())), - tap((res: any) => { - // *info: once isAuthenticated$ responses , SSO sessiong is loaded - this.loading$.next(false); - this.loggedIn = res; - }) - ); - handleRedirectCallback$ = this.auth0Client$.pipe( - concatMap((client: Auth0Client) => - from(client.handleRedirectCallback(this.currentHref)) - ) - ); - // Create subject and public observable of user profile data - private userProfileSubject$ = new BehaviorSubject(undefined); - userProfile$ = this.userProfileSubject$.asObservable(); - // Create a local property for login status - loggedIn = false; - - constructor() { - // On initial load, check authentication state with authorization server - // Set up local auth streams if user is already authenticated - const params = this.currentHref; - if (params.includes('code=') && params.includes('state=')) { - this.handleAuthCallback(); - } else { - this.localAuthSetup(); - } - // this.handlerReturnToAferlogout(); - } - - handlerReturnToAferlogout() { - const { query } = querystring.parseUrl(this.currentHref); - const returnTo = query.returnTo; - if (returnTo) { - const target = this.getTargetRouteFromReturnTo(returnTo); - this.redirectRoot.next(target); - } - } - // When calling, options can be passed if desired - // https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser - getUser$(options?): Observable { - return this.auth0Client$.pipe( - concatMap((client: Auth0Client) => from(client.getUser(options))), - tap((user) => { - this.setSession(user); - this.userProfileSubject$.next(user); - }) - ); - } - - private localAuthSetup() { - // This should only be called on app initialization - // Set up local authentication streams - const checkAuth$ = this.isAuthenticated$.pipe( - concatMap((loggedIn: boolean) => { - if (loggedIn) { - // If authenticated, get user and set in app - // NOTE: you could pass options here if needed - return this.getUser$(); - } - this.auth0Client$ - .pipe(concatMap((client: Auth0Client) => from(client.checkSession()))) - .subscribe((data) => { }); - this.userProfileSubject$.next(null); - // If not authenticated, return stream that emits 'false' - return of(loggedIn); - }) - ); - checkAuth$.subscribe(); - } - - login(redirectPath: string = '/') { - // A desired redirect path can be passed to login method - // (e.g., from a route guard) - // Ensure Auth0 client instance exists - this.auth0Client$.subscribe((client: Auth0Client) => { - // Call method to log in - client.loginWithRedirect({ - redirect_uri: `${window.location.origin}${window.location.search}`, - appState: { target: redirectPath }, - }); - }); - } - - private getTargetRouteFromAppState(appState) { - if (!appState) { - return '/'; - } - - const { returnTo, target, targetUrl } = appState; - - return ( - this.getTargetRouteFromReturnTo(returnTo) || target || targetUrl || '/' - ); - } - - private getTargetRouteFromReturnTo(returnTo) { - if (!returnTo) { - return ''; - } - - const { fragmentIdentifier } = querystring.parseUrl(returnTo, { - parseFragmentIdentifier: true, - }); - - if (fragmentIdentifier) { - return fragmentIdentifier; - } - - const { pathname } = new Url(returnTo); - return pathname || '/'; - } - - private handleAuthCallback() { - // Call when app reloads after user logs in with Auth0 - const params = this.currentHref; - if (params.includes('code=') && params.includes('state=')) { - let targetRoute: string; // Path to redirect to after login processsed - const authComplete$ = this.handleRedirectCallback$.pipe( - // Have client, now call method to handle auth callback redirect - tap((cbRes: any) => { - targetRoute = this.getTargetRouteFromAppState(cbRes.appState); - }), - concatMap(() => { - // Redirect callback complete; get user and login status - return combineLatest([this.getUser$(), this.isAuthenticated$]); - }) - ); - // Subscribe to authentication completion observable - // Response will be an array of user and login status - authComplete$.subscribe(() => { - // console.log('navigating too', { - // current: this.currentHref, - // targetRoute, - // href: window.location.href, - // }); - // Redirect to target route after callback processing - // *info: this url change will remove the code and state from the URL - // * this is need to avoid invalid state in the next refresh - this.redirectRoot.next(targetRoute); - // this.router.navigate([targetRoute]); - }); - } - } - - logout() { - this.auth0Client$.subscribe((client: Auth0Client) => { - // Call method to log out - client.logout({ - client_id: this.auth0Options.clientId, - returnTo: EnvConfig['landing-page'], - }); - }); - } - - getTokenSilently$(options?): Observable { - return this.auth0Client$.pipe( - concatMap((client: Auth0Client) => from(client.getTokenSilently(options))) - ); - } - - public getIdToken(): Promise { - return new Promise((resolve, reject) => { - const token = this.getIdToken$({ ignoreCache: true }).toPromise(); - resolve(token); - }); - } - - private setSession(authResult): void { - localStorage.setItem('userid', authResult.nickname); - localStorage.setItem('user_email', authResult.email); - localStorage.setItem('user_name', authResult.name); - } - - getIdToken$(options?): Observable { - return this.auth0Client$.pipe( - // *info: if getIdToken fails , just return empty in the catchError - concatMap((client: Auth0Client) => - from(client.getIdTokenClaims(options)) - ), - concatMap((claims: any) => of((claims && claims.__raw) || '')), - catchError(() => of('')) - ); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/services/auth.utils.ts b/cla-frontend-contributor-console/src/ionic/services/auth.utils.ts deleted file mode 100644 index 01ed3cf21..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/auth.utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -export const AUTH_ROUTE = '/auth'; diff --git a/cla-frontend-contributor-console/src/ionic/services/cla.env.utils.ts b/cla-frontend-contributor-console/src/ionic/services/cla.env.utils.ts deleted file mode 100644 index 9f435d64a..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/cla.env.utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import * as env from '../../config/cla-env-config.json'; -export const EnvConfig = env as any; diff --git a/cla-frontend-contributor-console/src/ionic/services/cla.service.ts b/cla-frontend-contributor-console/src/ionic/services/cla.service.ts deleted file mode 100644 index 8fc6c5dfe..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/cla.service.ts +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; - -import 'rxjs/Rx'; - -@Injectable() -export class ClaService { - http: any; - claApiUrl: string = ''; - v4ApiUrl: string = ''; - localTesting = false; - v1ClaAPIURLLocal = 'http://localhost:5000'; - v2ClaAPIURLLocal = 'http://localhost:5000'; - v3ClaAPIURLLocal = 'http://localhost:8080'; - v4ClaAPIURLLocal = 'http://localhost:8080'; - - constructor(http: Http) { - this.http = http; - } - - /** - * Constructs a URL based on the path and endpoint host:port. - * @param path the URL path - * @returns a URL to the V1 endpoint with the specified path. If running in local mode, the endpoint will point to a - * local host:port - otherwise the endpoint will point the appropriate environment endpoint running in the cloud. - */ - private getV1Endpoint(path: string) { - let url: URL; - if (this.localTesting) { - url = new URL(this.v1ClaAPIURLLocal + path); - } else { - url = new URL(this.claApiUrl + path); - } - return url; - } - - /** - * Constructs a URL based on the path and endpoint host:port. - * @param path the URL path - * @returns a URL to the V2 endpoint with the specified path. If running in local mode, the endpoint will point to a - * local host:port - otherwise the endpoint will point the appropriate environment endpoint running in the cloud. - */ - private getV2Endpoint(path: string) { - let url: URL; - if (this.localTesting) { - url = new URL(this.v2ClaAPIURLLocal + path); - } else { - url = new URL(this.claApiUrl + path); - } - return url; - } - - - private getV4Endpoint(path: string) { - let url: URL; - if (this.localTesting) { - url = new URL(this.v2ClaAPIURLLocal + path); - } else { - url = new URL(this.v4ApiUrl + path); - } - return url; - } - - /** - * Constructs a URL based on the path and endpoint host:port. - * @param path the URL path - * @returns a URL to the V3 endpoint with the specified path. If running in local mode, the endpoint will point to a - * local host:port - otherwise the endpoint will point the appropriate environment endpoint running in the cloud. - */ - private getV3Endpoint(path: string) { - let url: URL; - if (this.localTesting) { - url = new URL(this.v3ClaAPIURLLocal + path); - } else { - url = new URL(this.claApiUrl + path); - } - return url; - } - - public isLocalTesting(flag: boolean) { - if (flag) { - console.log('Running in local services mode'); - } else { - console.log('Running in deployed services mode'); - } - this.localTesting = flag; - } - - public setApiUrl(claApiUrl: string) { - this.claApiUrl = claApiUrl; - } - - public setV4ApiUrl(v4ApiUrl: string) { - this.v4ApiUrl = v4ApiUrl + '/cla-service'; - } - - public setHttp(http: any) { - this.http = http; // allow configuration for alternate http library - } - - /** - * /user/{user_id} - */ - getUser(userId) { - const url: URL = this.getV2Endpoint('/v2/user/' + userId); - return this.http.get(url).map((res) => res.json()); - } - - getUserWithAuthToken(userId) { - const url: URL = this.getV2Endpoint('/v2/user/' + userId); - return this.http.securedGet(url).map((res) => res.json()); - } - - // creates a new account for Gerrit users, with email. - postOrGetUserForGerrit() { - const url: URL = this.getV1Endpoint('/v1/user/gerrit'); - return this.http.securedPost(url).map((res) => res.json()); - } - - /** - * Request to be added to the company Approved List (formerly WhiteList) - * - * /user/{user_id}/request-company-whitelist/{company_id} - */ - requestToBeOnCompanyApprovedList(userId, companyId, projectId, data) { - //const url: URL = this.getV2Endpoint('/v2/user/' + userId + '/request-company-whitelist/' + companyId); - const url: URL = this.getV3Endpoint(`/v3/company/${companyId}/ccla-whitelist-requests/${projectId}`); - return this.http.post(url, data);// no response .map((res) => res.json()); - } - - /** - * /user/{user_id}/invite-company-admin - */ - postEmailToCompanyAdmin(userId, data) { - const url: URL = this.getV4Endpoint('/v4/user/' + userId + '/request-company-admin'); - return this.http.post(url, data).map((res) => res); - } - - /** - * /user/{user_id}/active-signature - */ - getUserSignatureIntent(userId) { - const url: URL = this.getV2Endpoint('/v2/user/' + userId + '/active-signature'); - return this.http.get(url).map((res) => res.json()); - } - - /** - * /user/{user_id}/project/{project_id}/last-signature - **/ - - getLastIndividualSignature(userId, projectId) { - const url: URL = this.getV2Endpoint('/v2/user/' + userId + '/project/' + projectId + '/last-signature'); - return this.http.get(url).map((res) => res.json()); - } - - /** - * GET /v3/signatures/project/{project_id}/company/{company_id} - * - * @param companyId the company ID - * @param projectId the project ID - * @param pageSize the optional page size - default is 50 - * @param nextKey the next key used when asking for the next page of results - */ - getCompanyProjectSignatures(companyId, projectId, pageSize = 50, nextKey = '') { - let path: string = '/v3/signatures/project/' + projectId + '/company/' + companyId + '?pageSize=' + pageSize; - if (nextKey != null && nextKey !== '' && nextKey.trim().length > 0) { - path += '&nextKey=' + nextKey; - } - const url: URL = this.getV3Endpoint(path); - return this.http.getWithCreds(url).map((res) => res.json()); - } - - getAllCompanies() { - const url: URL = this.getV2Endpoint('/v2/company'); - return this.http.get(url).map((res) => res.json()); - } - - /** - * /company/{company_id} - **/ - getCompany(companyId) { - const url: URL = this.getV2Endpoint('/v2/company/' + companyId); - return this.http.get(url).map((res) => res.json()); - } - - /** - * /project/{project_id} - **/ - getProject(projectId) { - const url: URL = this.getV2Endpoint('/v2/project/' + projectId); - return this.http.get(url).map((res) => res.json()); - } - - getProjectWithAuthToken(projectId) { - const url: URL = this.getV2Endpoint('/v2/project/' + projectId); - return this.http.securedGet(url).map((res) => res.json()); - } - - /** - * /request-individual-signature - **/ - postIndividualSignatureRequest(signatureRequest) { - const url: URL = this.getV2Endpoint('/v2/request-individual-signature'); - return this.http.post(url, signatureRequest).map((res) => res.json()); - } - - /** - * /check-prepare-employee-signature - **/ - postCheckAndPreparedEmployeeSignature(data) { - const url: URL = this.getV2Endpoint('/v2/check-prepare-employee-signature'); - return this.http.post(url, data).map((res) => res.json()); - } - - /** - * /request-employee-signature - **/ - postEmployeeSignatureRequest(signatureRequest) { - const url: URL = this.getV2Endpoint('/v2/request-employee-signature'); - return this.http.post(url, signatureRequest).map((res) => res.json()); - } - - /** - * /company/{companyID}/ccla-whitelist-requests/{projectID} - */ - postCCLAWhitelistRequest(companyID, projectID, user) { - const url: URL = this.getV3Endpoint('/v3/company/' + companyID + '/ccla-whitelist-requests/' + projectID); - return this.http.post(url, user); - } - - getGerrit(gerritId) { - const url: URL = this.getV2Endpoint('/v2/gerrit/' + gerritId); - return this.http.securedGet(url).map((res) => res.json()); - } - - getProjectGerrits(projectId) { - const url: URL = this.getV1Endpoint('/v1/project/' + projectId + '/gerrits'); - return this.http.securedGet(url).map((res) => res.json()); - } - - getReleaseVersion() { - const url: URL = this.getV3Endpoint('/v3/ops/version'); - return this.http.getWithoutAuth(url).map((res) => res.json()); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/services/constants.ts b/cla-frontend-contributor-console/src/ionic/services/constants.ts deleted file mode 100644 index 3c6fc56af..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -export const CINCO_API_URL: string = 'https://cinco_api_endpoint'; -export const CLA_API_URL: string = 'https://cla_api_endpoint/dev-runze'; -export const ANALYTICS_API_URL: string = 'https://analytics_api_endpoint'; diff --git a/cla-frontend-contributor-console/src/ionic/services/http-client.ts b/cla-frontend-contributor-console/src/ionic/services/http-client.ts deleted file mode 100644 index 31a044f5d..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/http-client.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Injectable } from '@angular/core'; -import { Headers, Http } from '@angular/http'; -import { KeycloakService } from './keycloak/keycloak.service'; -import { Observable } from 'rxjs/Rx'; -import { AuthService } from './auth.service'; - -@Injectable() -export class HttpClient { - constructor( - public http: Http, - private keycloak: KeycloakService, - private authService: AuthService - ) { } - - private buildAuthHeaders(contentType: string = 'application/json') { - let headers = new Headers({ - Accept: 'application/json', - 'Content-Type': contentType - }); - return this.authService.getIdToken().then((token) => { - if (token) { - headers.append('Authorization', 'Bearer ' + token); - return headers; - } - }); - } - - private buildWithoutAuthHeaders(contentType: string = 'application/json') { - let headers = new Headers({ - Accept: 'application/json', - 'Content-Type': contentType - }); - return Promise.resolve(headers); - } - - getWithoutAuth(url) { - return Observable.fromPromise(this.buildWithoutAuthHeaders()).switchMap((headers) => - this.http.get(url, { headers: headers }) - ); - } - - setHttp(http: Http) { - this.http = http; // allow alternate http library - } - - buildHeaders(contentType: string = 'application/json') { - let headers = new Headers({ - Accept: 'application/json', - 'Content-Type': contentType - }); - return Promise.resolve(headers); - } - - get(url) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.get(url, { headers: headers }) - ); - } - - getWithCreds(url) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.get(url, { headers: headers, withCredentials: true }) - ); - } - - post(url, data) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.post(url, data, { headers: headers }) - ); - } - - postWithCreds(url, data) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.post(url, data, { headers: headers, withCredentials: true }) - ); - } - - put(url, data, contentType: string = 'application/json') { - return Observable.fromPromise(this.buildAuthHeaders(contentType)).switchMap((headers) => - this.http.put(url, data, { headers: headers }) - ); - } - - putWithCreds(url, data, contentType: string = 'application/json') { - return Observable.fromPromise(this.buildAuthHeaders(contentType)).switchMap((headers) => - this.http.put(url, data, { headers: headers, withCredentials: true }) - ); - } - - patch(url, data, contentType: string = 'application/json') { - return Observable.fromPromise(this.buildAuthHeaders(contentType)).switchMap((headers) => - this.http.patch(url, data, { headers: headers }) - ); - } - - delete(url) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.delete(url, { headers: headers }) - ); - } - - deleteWithBody(url, body) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.delete(url, { body: body, headers: headers }) - ); - } - - deleteWithCredsAndBody(url, body) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.delete(url, { body: body, headers: headers, withCredentials: true }) - ); - } - securedGet(url) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.get(url, { headers: headers }) - ); - } - - securedPost(url, data) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.post(url, data, { headers: headers }) - ); - } - - securedPut(url, data, contentType: string = 'application/json') { - return Observable.fromPromise(this.buildAuthHeaders(contentType)).switchMap((headers) => - this.http.put(url, data, { headers: headers }) - ); - } - - securedPatch(url, data, contentType: string = 'application/json') { - return Observable.fromPromise(this.buildAuthHeaders(contentType)).switchMap((headers) => - this.http.patch(url, data, { headers: headers }) - ); - } - - securedDelete(url) { - return Observable.fromPromise(this.buildAuthHeaders()).switchMap((headers) => - this.http.delete(url, { headers: headers }) - ); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.d.ts b/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.d.ts deleted file mode 100644 index 1a84aa8f1..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.d.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2017 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -declare module KeycloakModule { - export interface Promise { - success(callback: Function): Promise; - error(callback: Function): Promise; - } - - export type ResponseModes = 'query' | 'fragment'; - export type Flows = 'standard' | 'implicit' | 'hybrid'; - - export interface InitOptions { - checkLoginIframe?: boolean; - checkLoginIframeInterval?: number; - onLoad?: string; - adapter?: string; - responseMode?: ResponseModes; - flow?: Flows; - token?: string; - refreshToken?: string; - idToken?: string; - timeSkew?: number; - } - - export interface LoginOptions { - redirectUri?: string; - prompt?: string; - maxAge?: number; - loginHint?: string; - action?: string; - locale?: string; - } - - export interface LogoutOptions { - redirectUri?: string; - } - - export interface RedirectUriOptions { - redirectUri?: string; - } - - export interface KeycloakClient { - init(options?: InitOptions): Promise; - login(options?: LoginOptions): Promise; - logout(options?: LogoutOptions): Promise; - createLoginUrl(options?: LoginOptions): string; - updateToken(options?: RedirectUriOptions): Promise; - createLogoutUrl(options?: RedirectUriOptions): string; - register(options?: LoginOptions): Promise; - createRegisterUrl(options?: RedirectUriOptions): string; - accountManagement(): Promise; - createAccountUrl(options?: RedirectUriOptions): string; - hasRealmRole(role: string): boolean; - hasResourceRole(role: string, resource?: string): boolean; - loadUserProfile(): Promise; - isTokenExpired(minValidity: number): boolean; - updateToken(minValidity: number): Promise; - clearToken(): any; - - realm: string; - clientId: string; - authServerUrl: string; - - token: string; - tokenParsed: any; - refreshToken: string; - refreshTokenParsed: any; - idToken: string; - idTokenParsed: any; - realmAccess: any; - resourceAccess: any; - authenticated: boolean; - subject: string; - timeSkew: number; - responseMode: ResponseModes; - flow: Flows; - responseType: string; - - onReady: Function; - onAuthSuccess: Function; - onAuthError: Function; - onAuthRefreshSuccess: Function; - onAuthRefreshError: Function; - onAuthLogout: Function; - onTokenExpired: Function; - } -} - -declare var Keycloak: { - new (config?: any): KeycloakModule.KeycloakClient; -}; diff --git a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.http.ts b/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.http.ts deleted file mode 100644 index c611ba43b..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.http.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Injectable } from '@angular/core'; -import { - Http, - Request, - XHRBackend, - ConnectionBackend, - RequestOptions, - RequestOptionsArgs, - Response, - Headers -} from '@angular/http'; - -import { KeycloakService } from './keycloak.service'; -import { Observable } from 'rxjs/Rx'; - -/** - * This provides a wrapper over the ng2 Http class that insures tokens are refreshed on each request. - */ -@Injectable() -export class KeycloakHttp extends Http { - constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions, private _keycloakService: KeycloakService) { - super(_backend, _defaultOptions); - } - - request(url: string | Request, options?: RequestOptionsArgs): Observable { - if (!this._keycloakService.authenticated()) return super.request(url, options); - - const tokenPromise: Promise = this._keycloakService.getToken(); - const tokenObservable: Observable = Observable.fromPromise(tokenPromise); - - if (typeof url === 'string') { - return tokenObservable - .map((token) => { - const authOptions = new RequestOptions({ headers: new Headers({ Authorization: 'Bearer ' + token }) }); - return new RequestOptions().merge(options).merge(authOptions); - }) - .concatMap((opts) => super.request(url, opts)); - } else if (url instanceof Request) { - return tokenObservable - .map((token) => { - url.headers.set('Authorization', 'Bearer ' + token); - return url; - }) - .concatMap((request) => super.request(request)); - } - } -} - -export function keycloakHttpFactory( - backend: XHRBackend, - defaultOptions: RequestOptions, - keycloakService: KeycloakService -) { - return new KeycloakHttp(backend, defaultOptions, keycloakService); -} - -export const KEYCLOAK_HTTP_PROVIDER = { - provide: Http, - useFactory: keycloakHttpFactory, - deps: [XHRBackend, RequestOptions, KeycloakService] -}; diff --git a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.js b/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.js deleted file mode 100644 index 7534e0826..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.js +++ /dev/null @@ -1,1327 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -(function(window, undefined) { - var Keycloak = function(config) { - if (!(this instanceof Keycloak)) { - return new Keycloak(config); - } - - var kc = this; - var adapter; - var refreshQueue = []; - var callbackStorage; - - var loginIframe = { - enable: true, - callbackList: [], - interval: 5 - }; - - var scripts = document.getElementsByTagName('script'); - for (var i = 0; i < scripts.length; i++) { - if ( - (scripts[i].src.indexOf('keycloak.js') !== -1 || scripts[i].src.indexOf('keycloak.min.js') !== -1) && - scripts[i].src.indexOf('version=') !== -1 - ) { - kc.iframeVersion = scripts[i].src.substring(scripts[i].src.indexOf('version=') + 8).split('&')[0]; - } - } - - kc.init = function(initOptions) { - kc.authenticated = false; - - callbackStorage = createCallbackStorage(); - - if (initOptions && initOptions.adapter === 'cordova') { - adapter = loadAdapter('cordova'); - } else if (initOptions && initOptions.adapter === 'default') { - adapter = loadAdapter(); - } else { - if (window.Cordova || window.cordova) { - adapter = loadAdapter('cordova'); - } else { - adapter = loadAdapter(); - } - } - - if (initOptions) { - if (typeof initOptions.checkLoginIframe !== 'undefined') { - loginIframe.enable = initOptions.checkLoginIframe; - } - - if (initOptions.checkLoginIframeInterval) { - loginIframe.interval = initOptions.checkLoginIframeInterval; - } - - if (initOptions.onLoad === 'login-required') { - kc.loginRequired = true; - } - - if (initOptions.responseMode) { - if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') { - kc.responseMode = initOptions.responseMode; - } else { - throw 'Invalid value for responseMode'; - } - } - - if (initOptions.flow) { - switch (initOptions.flow) { - case 'standard': - kc.responseType = 'code'; - break; - case 'implicit': - kc.responseType = 'id_token token'; - break; - case 'hybrid': - kc.responseType = 'code id_token token'; - break; - default: - throw 'Invalid value for flow'; - } - kc.flow = initOptions.flow; - } - - if (initOptions.timeSkew != null) { - kc.timeSkew = initOptions.timeSkew; - } - } - - if (!kc.responseMode) { - kc.responseMode = 'fragment'; - } - if (!kc.responseType) { - kc.responseType = 'code'; - kc.flow = 'standard'; - } - - var promise = createPromise(); - - var initPromise = createPromise(); - initPromise.promise - .success(function() { - kc.onReady && kc.onReady(kc.authenticated); - promise.setSuccess(kc.authenticated); - }) - .error(function(errorData) { - promise.setError(errorData); - }); - - var configPromise = loadConfig(config); - - function onLoad() { - var doLogin = function(prompt) { - if (!prompt) { - options.prompt = 'none'; - } - kc.login(options) - .success(function() { - initPromise.setSuccess(); - }) - .error(function() { - initPromise.setError(); - }); - }; - - var options = {}; - switch (initOptions.onLoad) { - case 'check-sso': - if (loginIframe.enable) { - setupCheckLoginIframe().success(function() { - checkLoginIframe() - .success(function() { - doLogin(false); - }) - .error(function() { - initPromise.setSuccess(); - }); - }); - } else { - doLogin(false); - } - break; - case 'login-required': - doLogin(true); - break; - default: - throw 'Invalid value for onLoad'; - } - } - - function processInit() { - var callback = parseCallback(window.location.href); - - if (callback) { - setupCheckLoginIframe(); - window.history.replaceState({}, null, callback.newUrl); - processCallback(callback, initPromise); - return; - } else if (initOptions) { - if (initOptions.token && initOptions.refreshToken) { - setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken); - - if (loginIframe.enable) { - setupCheckLoginIframe().success(function() { - checkLoginIframe() - .success(function() { - kc.onAuthSuccess && kc.onAuthSuccess(); - initPromise.setSuccess(); - }) - .error(function() { - setToken(null, null, null); - initPromise.setSuccess(); - }); - }); - } else { - kc.updateToken(-1) - .success(function() { - kc.onAuthSuccess && kc.onAuthSuccess(); - initPromise.setSuccess(); - }) - .error(function() { - kc.onAuthError && kc.onAuthError(); - if (initOptions.onLoad) { - onLoad(); - } else { - initPromise.setError(); - } - }); - } - } else if (initOptions.onLoad) { - onLoad(); - } else { - initPromise.setSuccess(); - } - } else { - initPromise.setSuccess(); - } - } - - configPromise.success(processInit); - configPromise.error(function() { - promise.setError(); - }); - - return promise.promise; - }; - - kc.login = function(options) { - return adapter.login(options); - }; - - kc.createLoginUrl = function(options) { - var state = createUUID(); - var nonce = createUUID(); - - var redirectUri = adapter.redirectUri(options); - - var callbackState = { - state: state, - nonce: nonce, - redirectUri: encodeURIComponent(redirectUri) - }; - - if (options && options.prompt) { - callbackState.prompt = options.prompt; - } - - callbackStorage.add(callbackState); - - var action = 'auth'; - if (options && options.action == 'register') { - action = 'registrations'; - } - - var scope = options && options.scope ? 'openid ' + options.scope : 'openid'; - - var url = - getRealmUrl() + - '/protocol/openid-connect/' + - action + - '?client_id=' + - encodeURIComponent(kc.clientId) + - '&redirect_uri=' + - encodeURIComponent(redirectUri) + - '&state=' + - encodeURIComponent(state) + - '&nonce=' + - encodeURIComponent(nonce) + - '&response_mode=' + - encodeURIComponent(kc.responseMode) + - '&response_type=' + - encodeURIComponent(kc.responseType) + - '&scope=' + - encodeURIComponent(scope); - - if (options && options.prompt) { - url += '&prompt=' + encodeURIComponent(options.prompt); - } - - if (options && options.maxAge) { - url += '&max_age=' + encodeURIComponent(options.maxAge); - } - - if (options && options.loginHint) { - url += '&login_hint=' + encodeURIComponent(options.loginHint); - } - - if (options && options.idpHint) { - url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint); - } - - if (options && options.locale) { - url += '&ui_locales=' + encodeURIComponent(options.locale); - } - - return url; - }; - - kc.logout = function(options) { - return adapter.logout(options); - }; - - kc.createLogoutUrl = function(options) { - var url = - getRealmUrl() + - '/protocol/openid-connect/logout' + - '?redirect_uri=' + - encodeURIComponent(adapter.redirectUri(options, false)); - - return url; - }; - - kc.register = function(options) { - return adapter.register(options); - }; - - kc.createRegisterUrl = function(options) { - if (!options) { - options = {}; - } - options.action = 'register'; - return kc.createLoginUrl(options); - }; - - kc.createAccountUrl = function(options) { - var url = - getRealmUrl() + - '/account' + - '?referrer=' + - encodeURIComponent(kc.clientId) + - '&referrer_uri=' + - encodeURIComponent(adapter.redirectUri(options)); - - return url; - }; - - kc.accountManagement = function() { - return adapter.accountManagement(); - }; - - kc.hasRealmRole = function(role) { - var access = kc.realmAccess; - return !!access && access.roles.indexOf(role) >= 0; - }; - - kc.hasResourceRole = function(role, resource) { - if (!kc.resourceAccess) { - return false; - } - - var access = kc.resourceAccess[resource || kc.clientId]; - return !!access && access.roles.indexOf(role) >= 0; - }; - - kc.loadUserProfile = function() { - var url = getRealmUrl() + '/account'; - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.setRequestHeader('Accept', 'application/json'); - req.setRequestHeader('Authorization', 'bearer ' + kc.token); - - var promise = createPromise(); - - req.onreadystatechange = function() { - if (req.readyState == 4) { - if (req.status == 200) { - kc.profile = JSON.parse(req.responseText); - promise.setSuccess(kc.profile); - } else { - promise.setError(); - } - } - }; - - req.send(); - - return promise.promise; - }; - - kc.loadUserInfo = function() { - var url = getRealmUrl() + '/protocol/openid-connect/userinfo'; - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.setRequestHeader('Accept', 'application/json'); - req.setRequestHeader('Authorization', 'bearer ' + kc.token); - - var promise = createPromise(); - - req.onreadystatechange = function() { - if (req.readyState == 4) { - if (req.status == 200) { - kc.userInfo = JSON.parse(req.responseText); - promise.setSuccess(kc.userInfo); - } else { - promise.setError(); - } - } - }; - - req.send(); - - return promise.promise; - }; - - kc.isTokenExpired = function(minValidity) { - if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit')) { - throw 'Not authenticated'; - } - - if (kc.timeSkew == null) { - console.info('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set'); - return true; - } - - var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew; - if (minValidity) { - expiresIn -= minValidity; - } - return expiresIn < 0; - }; - - kc.updateToken = function(minValidity) { - var promise = createPromise(); - - if (!kc.refreshToken) { - promise.setError(); - return promise.promise; - } - - minValidity = minValidity || 5; - - var exec = function() { - var refreshToken = false; - if (minValidity == -1) { - refreshToken = true; - console.info('[KEYCLOAK] Refreshing token: forced refresh'); - } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) { - refreshToken = true; - console.info('[KEYCLOAK] Refreshing token: token expired'); - } - - if (!refreshToken) { - promise.setSuccess(false); - } else { - var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken; - var url = getRealmUrl() + '/protocol/openid-connect/token'; - - refreshQueue.push(promise); - - if (refreshQueue.length == 1) { - var req = new XMLHttpRequest(); - req.open('POST', url, true); - req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - req.withCredentials = false; - - if (kc.clientId && kc.clientSecret) { - req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); - } else { - params += '&client_id=' + encodeURIComponent(kc.clientId); - } - - var timeLocal = new Date().getTime(); - - req.onreadystatechange = function() { - if (req.readyState == 4) { - if (req.status == 200) { - console.info('[KEYCLOAK] Token refreshed'); - - timeLocal = (timeLocal + new Date().getTime()) / 2; - - var tokenResponse = JSON.parse(req.responseText); - - setToken( - tokenResponse['access_token'], - tokenResponse['refresh_token'], - tokenResponse['id_token'], - timeLocal - ); - - kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess(); - for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { - p.setSuccess(true); - } - } else { - console.warn('[KEYCLOAK] Failed to refresh token'); - - kc.onAuthRefreshError && kc.onAuthRefreshError(); - for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { - p.setError(true); - } - } - } - }; - - req.send(params); - } - } - }; - - if (loginIframe.enable) { - var iframePromise = checkLoginIframe(); - iframePromise - .success(function() { - exec(); - }) - .error(function() { - promise.setError(); - }); - } else { - exec(); - } - - return promise.promise; - }; - - kc.clearToken = function() { - if (kc.token) { - setToken(null, null, null); - kc.onAuthLogout && kc.onAuthLogout(); - if (kc.loginRequired) { - kc.login(); - } - } - }; - - function getRealmUrl() { - if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') { - return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm); - } else { - return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm); - } - } - - function getOrigin() { - if (!window.location.origin) { - return ( - window.location.protocol + - '//' + - window.location.hostname + - (window.location.port ? ':' + window.location.port : '') - ); - } else { - return window.location.origin; - } - } - - function processCallback(oauth, promise) { - var code = oauth.code; - var error = oauth.error; - var prompt = oauth.prompt; - - var timeLocal = new Date().getTime(); - - if (error) { - if (prompt != 'none') { - var errorData = { error: error, error_description: oauth.error_description }; - kc.onAuthError && kc.onAuthError(errorData); - promise && promise.setError(errorData); - } else { - promise && promise.setSuccess(); - } - return; - } else if (kc.flow != 'standard' && (oauth.access_token || oauth.id_token)) { - authSuccess(oauth.access_token, null, oauth.id_token, true); - } - - if (kc.flow != 'implicit' && code) { - var params = 'code=' + code + '&grant_type=authorization_code'; - var url = getRealmUrl() + '/protocol/openid-connect/token'; - - var req = new XMLHttpRequest(); - req.open('POST', url, true); - req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - - if (kc.clientId && kc.clientSecret) { - req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); - } else { - params += '&client_id=' + encodeURIComponent(kc.clientId); - } - - params += '&redirect_uri=' + oauth.redirectUri; - - req.withCredentials = false; - - req.onreadystatechange = function() { - if (req.readyState == 4) { - if (req.status == 200) { - var tokenResponse = JSON.parse(req.responseText); - authSuccess( - tokenResponse['access_token'], - tokenResponse['refresh_token'], - tokenResponse['id_token'], - kc.flow === 'standard' - ); - } else { - kc.onAuthError && kc.onAuthError(); - promise && promise.setError(); - } - } - }; - - req.send(params); - } - - function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) { - timeLocal = (timeLocal + new Date().getTime()) / 2; - - setToken(accessToken, refreshToken, idToken, timeLocal); - - if ( - (kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) || - (kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) || - (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce) - ) { - console.info('[KEYCLOAK] Invalid nonce, clearing token'); - kc.clearToken(); - promise && promise.setError(); - } else { - if (fulfillPromise) { - kc.onAuthSuccess && kc.onAuthSuccess(); - promise && promise.setSuccess(); - } - } - } - } - - function loadConfig(url) { - var promise = createPromise(); - var configUrl; - - if (!config) { - configUrl = 'keycloak.json'; - } else if (typeof config === 'string') { - configUrl = config; - } - - if (configUrl) { - var req = new XMLHttpRequest(); - req.open('GET', configUrl, true); - req.setRequestHeader('Accept', 'application/json'); - - req.onreadystatechange = function() { - if (req.readyState == 4) { - if (req.status == 200 || fileLoaded(req)) { - var config = JSON.parse(req.responseText); - - kc.authServerUrl = config['auth-server-url']; - kc.realm = config['realm']; - kc.clientId = config['resource']; - kc.clientSecret = (config['credentials'] || {})['secret']; - - promise.setSuccess(); - } else { - promise.setError(); - } - } - }; - - req.send(); - } else { - if (!config['url']) { - var scripts = document.getElementsByTagName('script'); - for (var i = 0; i < scripts.length; i++) { - if (scripts[i].src.match(/.*keycloak\.js/)) { - config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js')); - break; - } - } - } - - if (!config.realm) { - throw 'realm missing'; - } - - if (!config.clientId) { - throw 'clientId missing'; - } - - kc.authServerUrl = config.url; - kc.realm = config.realm; - kc.clientId = config.clientId; - kc.clientSecret = (config.credentials || {}).secret; - - promise.setSuccess(); - } - - return promise.promise; - } - - function fileLoaded(xhr) { - return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:'); - } - - function setToken(token, refreshToken, idToken, timeLocal) { - if (kc.tokenTimeoutHandle) { - clearTimeout(kc.tokenTimeoutHandle); - kc.tokenTimeoutHandle = null; - } - - if (refreshToken) { - kc.refreshToken = refreshToken; - kc.refreshTokenParsed = decodeToken(refreshToken); - } else { - delete kc.refreshToken; - delete kc.refreshTokenParsed; - } - - if (idToken) { - kc.idToken = idToken; - kc.idTokenParsed = decodeToken(idToken); - } else { - delete kc.idToken; - delete kc.idTokenParsed; - } - - if (token) { - kc.token = token; - kc.tokenParsed = decodeToken(token); - kc.sessionId = kc.tokenParsed.session_state; - kc.authenticated = true; - kc.subject = kc.tokenParsed.sub; - kc.realmAccess = kc.tokenParsed.realm_access; - kc.resourceAccess = kc.tokenParsed.resource_access; - - if (timeLocal) { - kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat; - } - - if (kc.timeSkew != null) { - console.info( - '[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds' - ); - - if (kc.onTokenExpired) { - var expiresIn = (kc.tokenParsed['exp'] - new Date().getTime() / 1000 + kc.timeSkew) * 1000; - console.info('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s'); - if (expiresIn <= 0) { - kc.onTokenExpired(); - } else { - kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn); - } - } - } - } else { - delete kc.token; - delete kc.tokenParsed; - delete kc.subject; - delete kc.realmAccess; - delete kc.resourceAccess; - - kc.authenticated = false; - } - } - - function decodeToken(str) { - str = str.split('.')[1]; - - str = str.replace('/-/g', '+'); - str = str.replace('/_/g', '/'); - switch (str.length % 4) { - case 0: - break; - case 2: - str += '=='; - break; - case 3: - str += '='; - break; - default: - throw 'Invalid token'; - } - - str = (str + '===').slice(0, str.length + (str.length % 4)); - str = str.replace(/-/g, '+').replace(/_/g, '/'); - - str = decodeURIComponent(escape(atob(str))); - - str = JSON.parse(str); - return str; - } - - function createUUID() { - var s = []; - var hexDigits = '0123456789abcdef'; - for (var i = 0; i < 36; i++) { - s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); - } - s[14] = '4'; - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); - s[8] = s[13] = s[18] = s[23] = '-'; - var uuid = s.join(''); - return uuid; - } - - kc.callback_id = 0; - - function createCallbackId() { - var id = ''; - return id; - } - - function parseCallback(url) { - var oauth = new CallbackParser(url, kc.responseMode).parseUri(); - var oauthState = callbackStorage.get(oauth.state); - - if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) { - oauth.redirectUri = oauthState.redirectUri; - oauth.storedNonce = oauthState.nonce; - oauth.prompt = oauthState.prompt; - - if (oauth.fragment) { - oauth.newUrl += '#' + oauth.fragment; - } - - return oauth; - } - } - - function createPromise() { - var p = { - setSuccess: function(result) { - p.success = true; - p.result = result; - if (p.successCallback) { - p.successCallback(result); - } - }, - - setError: function(result) { - p.error = true; - p.result = result; - if (p.errorCallback) { - p.errorCallback(result); - } - }, - - promise: { - success: function(callback) { - if (p.success) { - callback(p.result); - } else if (!p.error) { - p.successCallback = callback; - } - return p.promise; - }, - error: function(callback) { - if (p.error) { - callback(p.result); - } else if (!p.success) { - p.errorCallback = callback; - } - return p.promise; - } - } - }; - return p; - } - - function setupCheckLoginIframe() { - var promise = createPromise(); - - if (!loginIframe.enable) { - promise.setSuccess(); - return promise.promise; - } - - if (loginIframe.iframe) { - promise.setSuccess(); - return promise.promise; - } - - var iframe = document.createElement('iframe'); - loginIframe.iframe = iframe; - - iframe.onload = function() { - var realmUrl = getRealmUrl(); - if (realmUrl.charAt(0) === '/') { - loginIframe.iframeOrigin = getOrigin(); - } else { - loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8)); - } - promise.setSuccess(); - - setTimeout(check, loginIframe.interval * 1000); - }; - - var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; - if (kc.iframeVersion) { - src = src + '?version=' + kc.iframeVersion; - } - - iframe.setAttribute('src', src); - iframe.setAttribute('title', 'keycloak-session-iframe'); - iframe.style.display = 'none'; - document.body.appendChild(iframe); - - var messageCallback = function(event) { - if (event.origin !== loginIframe.iframeOrigin || loginIframe.iframe.contentWindow !== event.source) { - return; - } - - if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) { - return; - } - - if (event.data != 'unchanged') { - kc.clearToken(); - } - - var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length); - - for (var i = callbacks.length - 1; i >= 0; --i) { - var promise = callbacks[i]; - if (event.data == 'unchanged') { - promise.setSuccess(); - } else { - promise.setError(); - } - } - }; - - window.addEventListener('message', messageCallback, false); - - var check = function() { - checkLoginIframe(); - if (kc.token) { - setTimeout(check, loginIframe.interval * 1000); - } - }; - - return promise.promise; - } - - function checkLoginIframe() { - var promise = createPromise(); - - if (loginIframe.iframe && loginIframe.iframeOrigin) { - var msg = kc.clientId + ' ' + kc.sessionId; - loginIframe.callbackList.push(promise); - var origin = loginIframe.iframeOrigin; - if (loginIframe.callbackList.length == 1) { - loginIframe.iframe.contentWindow.postMessage(msg, origin); - } - } else { - promise.setSuccess(); - } - - return promise.promise; - } - - function loadAdapter(type) { - if (!type || type == 'default') { - return { - login: function(options) { - window.location.href = kc.createLoginUrl(options); - return createPromise().promise; - }, - - logout: function(options) { - window.location.href = kc.createLogoutUrl(options); - return createPromise().promise; - }, - - register: function(options) { - window.location.href = kc.createRegisterUrl(options); - return createPromise().promise; - }, - - accountManagement: function() { - window.location.href = kc.createAccountUrl(); - return createPromise().promise; - }, - - redirectUri: function(options, encodeHash) { - if (arguments.length == 1) { - encodeHash = true; - } - - if (options && options.redirectUri) { - return options.redirectUri; - } else if (kc.redirectUri) { - return kc.redirectUri; - } else { - var redirectUri = location.href; - if (location.hash && encodeHash) { - redirectUri = redirectUri.substring(0, location.href.indexOf('#')); - redirectUri += - (redirectUri.indexOf('?') == -1 ? '?' : '&') + - 'redirect_fragment=' + - encodeURIComponent(location.hash.substring(1)); - } - return redirectUri; - } - } - }; - } - - if (type == 'cordova') { - loginIframe.enable = false; - var cordovaOpenWindowWrapper = function(loginUrl, target, options) { - if (window.cordova && window.cordova.InAppBrowser) { - // Use inappbrowser for IOS and Android if available - return window.cordova.InAppBrowser.open(loginUrl, target, options); - } else { - return window.open(loginUrl, target, options); - } - }; - return { - login: function(options) { - var promise = createPromise(); - - var o = 'location=no'; - if (options && options.prompt == 'none') { - o += ',hidden=yes'; - } - - var loginUrl = kc.createLoginUrl(options); - var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', o); - var completed = false; - - ref.addEventListener('loadstart', function(event) { - if (event.url.indexOf('http://localhost') == 0) { - var callback = parseCallback(event.url); - processCallback(callback, promise); - ref.close(); - completed = true; - } - }); - - ref.addEventListener('loaderror', function(event) { - if (!completed) { - if (event.url.indexOf('http://localhost') == 0) { - var callback = parseCallback(event.url); - processCallback(callback, promise); - ref.close(); - completed = true; - } else { - promise.setError(); - ref.close(); - } - } - }); - - return promise.promise; - }, - - logout: function(options) { - var promise = createPromise(); - - var logoutUrl = kc.createLogoutUrl(options); - var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes'); - - var error; - - ref.addEventListener('loadstart', function(event) { - if (event.url.indexOf('http://localhost') == 0) { - ref.close(); - } - }); - - ref.addEventListener('loaderror', function(event) { - if (event.url.indexOf('http://localhost') == 0) { - ref.close(); - } else { - error = true; - ref.close(); - } - }); - - ref.addEventListener('exit', function(event) { - if (error) { - promise.setError(); - } else { - kc.clearToken(); - promise.setSuccess(); - } - }); - - return promise.promise; - }, - - register: function() { - var registerUrl = kc.createRegisterUrl(); - var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', 'location=no'); - ref.addEventListener('loadstart', function(event) { - if (event.url.indexOf('http://localhost') == 0) { - ref.close(); - } - }); - }, - - accountManagement: function() { - var accountUrl = kc.createAccountUrl(); - var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no'); - ref.addEventListener('loadstart', function(event) { - if (event.url.indexOf('http://localhost') == 0) { - ref.close(); - } - }); - }, - - redirectUri: function(options) { - return 'http://localhost'; - } - }; - } - - throw 'invalid adapter type: ' + type; - } - - var LocalStorage = function() { - if (!(this instanceof LocalStorage)) { - return new LocalStorage(); - } - - localStorage.setItem('kc-test', 'test'); - localStorage.removeItem('kc-test'); - - var cs = this; - - function clearExpired() { - var time = new Date().getTime(); - for (var i = 0; i < localStorage.length; i++) { - var key = localStorage.key(i); - if (key && key.indexOf('kc-callback-') == 0) { - var value = localStorage.getItem(key); - if (value) { - try { - var expires = JSON.parse(value).expires; - if (!expires || expires < time) { - localStorage.removeItem(key); - } - } catch (err) { - localStorage.removeItem(key); - } - } - } - } - } - - cs.get = function(state) { - if (!state) { - return; - } - - var key = 'kc-callback-' + state; - var value = localStorage.getItem(key); - if (value) { - localStorage.removeItem(key); - value = JSON.parse(value); - } - - clearExpired(); - return value; - }; - - cs.add = function(state) { - clearExpired(); - - var key = 'kc-callback-' + state.state; - state.expires = new Date().getTime() + 60 * 60 * 1000; - localStorage.setItem(key, JSON.stringify(state)); - }; - }; - - var CookieStorage = function() { - if (!(this instanceof CookieStorage)) { - return new CookieStorage(); - } - - var cs = this; - - cs.get = function(state) { - if (!state) { - return; - } - - var value = getCookie('kc-callback-' + state); - setCookie('kc-callback-' + state, '', cookieExpiration(-100)); - if (value) { - return JSON.parse(value); - } - }; - - cs.add = function(state) { - setCookie('kc-callback-' + state.state, JSON.stringify(state), cookieExpiration(60)); - }; - - cs.removeItem = function(key) { - setCookie(key, '', cookieExpiration(-100)); - }; - - var cookieExpiration = function(minutes) { - var exp = new Date(); - exp.setTime(exp.getTime() + minutes * 60 * 1000); - return exp; - }; - - var getCookie = function(key) { - var name = key + '='; - var ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); - } - } - return ''; - }; - - var setCookie = function(key, value, expirationDate) { - var cookie = key + '=' + value + '; ' + 'expires=' + expirationDate.toUTCString() + '; '; - document.cookie = cookie; - }; - }; - - function createCallbackStorage() { - try { - return new LocalStorage(); - } catch (err) {} - - return new CookieStorage(); - } - - var CallbackParser = function(uriToParse, responseMode) { - if (!(this instanceof CallbackParser)) { - return new CallbackParser(uriToParse, responseMode); - } - var parser = this; - - var initialParse = function() { - var baseUri = null; - var queryString = null; - var fragmentString = null; - - var questionMarkIndex = uriToParse.indexOf('?'); - var fragmentIndex = uriToParse.indexOf('#', questionMarkIndex + 1); - if (questionMarkIndex == -1 && fragmentIndex == -1) { - baseUri = uriToParse; - } else if (questionMarkIndex != -1) { - baseUri = uriToParse.substring(0, questionMarkIndex); - queryString = uriToParse.substring(questionMarkIndex + 1); - if (fragmentIndex != -1) { - fragmentIndex = queryString.indexOf('#'); - fragmentString = queryString.substring(fragmentIndex + 1); - queryString = queryString.substring(0, fragmentIndex); - } - } else { - baseUri = uriToParse.substring(0, fragmentIndex); - fragmentString = uriToParse.substring(fragmentIndex + 1); - } - - return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString }; - }; - - var parseParams = function(paramString) { - var result = {}; - var params = paramString.split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - var paramName = decodeURIComponent(p[0]); - var paramValue = decodeURIComponent(p[1]); - result[paramName] = paramValue; - } - return result; - }; - - var handleQueryParam = function(paramName, paramValue, oauth) { - var supportedOAuthParams = ['code', 'state', 'error', 'error_description']; - - for (var i = 0; i < supportedOAuthParams.length; i++) { - if (paramName === supportedOAuthParams[i]) { - oauth[paramName] = paramValue; - return true; - } - } - return false; - }; - - parser.parseUri = function() { - var parsedUri = initialParse(); - - var queryParams = {}; - if (parsedUri.queryString) { - queryParams = parseParams(parsedUri.queryString); - } - - var oauth = { newUrl: parsedUri.baseUri }; - for (var param in queryParams) { - switch (param) { - case 'redirect_fragment': - oauth.fragment = queryParams[param]; - break; - default: - if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) { - oauth.newUrl += - (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]); - } - break; - } - } - - if (responseMode === 'fragment') { - var fragmentParams = {}; - if (parsedUri.fragmentString) { - fragmentParams = parseParams(parsedUri.fragmentString); - } - for (var param in fragmentParams) { - oauth[param] = fragmentParams[param]; - } - } - - return oauth; - }; - }; - }; - - if (typeof module === 'object' && module && typeof module.exports === 'object') { - module.exports = Keycloak; - } else { - window.Keycloak = Keycloak; - - if (typeof define === 'function' && define.amd) { - define('keycloak', [], function() { - return Keycloak; - }); - } - } -})(window); diff --git a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.service.ts b/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.service.ts deleted file mode 100644 index 8bb2c4b75..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/keycloak/keycloak.service.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2017 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// - -import { Injectable } from '@angular/core'; - -var Keycloak = require('./keycloak'); // load keycloak.js locally -type KeycloakClient = KeycloakModule.KeycloakClient; - -@Injectable() -export class KeycloakService { - static keycloakAuth: KeycloakClient = Keycloak('assets/keycloak.json'); - - static init(options?: any): Promise { - return new Promise((resolve, reject) => { - KeycloakService.keycloakAuth - .init(options) - .success(() => { - resolve(); - }) - .error((errorData: any) => { - reject(errorData); - }); - }); - } - - authenticated = (): boolean => { - return KeycloakService.keycloakAuth.authenticated; - }; - - login() { - KeycloakService.keycloakAuth.login(); - } - - logout = () => { - return KeycloakService.keycloakAuth.logout(); - }; - - account() { - KeycloakService.keycloakAuth.accountManagement(); - } - - createLogoutUrl = () => { - return KeycloakService.keycloakAuth.createLogoutUrl(); - }; - - profile(): Promise { - return new Promise((resolve, reject) => { - KeycloakService.keycloakAuth - .loadUserProfile() - .success((profile: any) => { - resolve(profile); - }) - .error((errorData: any) => { - reject(errorData); - }); - }); - } - - getToken(): Promise { - return new Promise((resolve, reject) => { - if (KeycloakService.keycloakAuth.token) { - KeycloakService.keycloakAuth - .updateToken(5) - .success(() => { - resolve(KeycloakService.keycloakAuth.token); - }) - .error(() => { - this.login(); - return reject('Failed to refresh token'); - }); - } else { - this.login(); - return reject('Not logged in'); - } - }); - } - - getTokenParsed(): Promise { - return new Promise((resolve, reject) => { - if (KeycloakService.keycloakAuth.tokenParsed) { - KeycloakService.keycloakAuth - .updateToken(5) - .success(() => { - resolve(KeycloakService.keycloakAuth.tokenParsed); - }) - .error(() => { - this.login(); - return reject('Failed to refresh token'); - }); - } else { - this.login(); - return reject('Not logged in'); - } - }); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/services/lfx-header.service.ts b/cla-frontend-contributor-console/src/ionic/services/lfx-header.service.ts deleted file mode 100644 index 0d6610b23..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/lfx-header.service.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import { Injectable } from '@angular/core'; -import { AuthService } from './auth.service'; - -@Injectable() -export class LfxHeaderService { - - constructor( - private auth: AuthService - ) { - this.setUserInLFxHeader(); - } - - setUserInLFxHeader(): void { - setTimeout(() => { - const lfHeaderEl: any = document.getElementById('lfx-header'); - if (!lfHeaderEl) { - return; - } - this.auth.userProfile$.subscribe((data) => { - if (data) { - lfHeaderEl.authuser = data; - } - }); - }, 1000); - } -} diff --git a/cla-frontend-contributor-console/src/ionic/services/roles.service.ts b/cla-frontend-contributor-console/src/ionic/services/roles.service.ts deleted file mode 100644 index b3951450e..000000000 --- a/cla-frontend-contributor-console/src/ionic/services/roles.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { AuthService } from './auth.service'; - -@Injectable() -export class RolesService { - // public userAuthenticated: boolean; - // public userRoleDefaults: any; - // public userRoles: any; - // private getDataObserver: any; - // public getData: any; - // private rolesFetched: boolean; - - // private LF_USERNAME_CLAIM = 'https://sso.linuxfoundation.org/claims/username'; - // private CLA_PROJECT_ADMIN = 'cla-system-admin'; - - // constructor(private authService: AuthService) { - // this.rolesFetched = false; - // this.userRoleDefaults = { - // isAuthenticated: this.authService.isAuthenticated(), - // isPmcUser: false, - // isStaffInc: false, - // isDirectorInc: false, - // isStaffDirect: false, - // isDirectorDirect: false, - // isExec: false, - // isAdmin: false - // }; - // this.userRoles = this.userRoleDefaults; - // } - - // ////////////////////////////////////////////////////////////////////////////// - - // /** - // * This service should ONLY contain methods for user roles - // **/ - - // ////////////////////////////////////////////////////////////////////////////// - // ////////////////////////////////////////////////////////////////////////////// - - // getUserRolesPromise() { - // console.log('Get UserRole Promise.'); - // if (this.authService.isAuthenticated()) { - // return this.authService - // .getIdToken() - // .then((token) => { - // return this.authService.parseIdToken(token); - // }) - // .then((tokenParsed) => { - // if (tokenParsed && tokenParsed[this.LF_USERNAME_CLAIM]) { - // this.userRoles = { - // isAuthenticated: this.authService.isAuthenticated(), - // isPmcUser: false, - // isStaffInc: false, - // isDirectorInc: false, - // isStaffDirect: false, - // isDirectorDirect: false, - // isExec: false, - // isAdmin: false - // }; - - // return this.userRoles; - // } - - // return this.userRoleDefaults; - // }) - // .catch((error) => { - // return Promise.resolve(this.userRoleDefaults); - // }); - // } else { - // // not authenticated. can't decode token. just return defaults - // return Promise.resolve(this.userRoleDefaults); - // } - // } - - // private isInArray(roles, role) { - // for (let i = 0; i < roles.length; i++) { - // if (roles[i].toLowerCase() === role.toLowerCase()) { - // return true; - // } - // } - // return false; - // } - - ////////////////////////////////////////////////////////////////////////////// -} diff --git a/cla-frontend-contributor-console/src/ionic/theme/footer.scss b/cla-frontend-contributor-console/src/ionic/theme/footer.scss deleted file mode 100644 index c1a711204..000000000 --- a/cla-frontend-contributor-console/src/ionic/theme/footer.scss +++ /dev/null @@ -1,36 +0,0 @@ -ion-footer { - background-image: none; - background-color: #4c79b6; - color: #ffffff; - .grid { - padding: 0; - .col { - padding: 15px 15px; - } - } - a { - color: #ffffff; - text-decoration: none; - } - - hr { - border: 0 none; - border-bottom: 1px solid #ffffff; - margin: 0; - } - picture, - img { - margin: -10px 0 -10px -13px; - width: 175px; - padding: 1rem; - } - button { - background-color: #ffa400; - padding: 8px 12px; - color: #ffffff; - font-size: 12px; - border-radius: 200px; - cursor: pointer; - border: 0; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/theme/modal.scss b/cla-frontend-contributor-console/src/ionic/theme/modal.scss deleted file mode 100644 index 1b61fd252..000000000 --- a/cla-frontend-contributor-console/src/ionic/theme/modal.scss +++ /dev/null @@ -1,28 +0,0 @@ -ion-modal { - .grid { - padding: 0 16px; - } - .toolbar { - padding: 1rem 1rem 1rem 2rem; - } - .toolbar { - background-color: #f5f5f5; - color: black; - } - .footer-seamless { - background-color: transparent; - border-color: transparent; - &:before { - display: none; - } - .toolbar { - background-color: inherit; - } - .toolbar-background { - border-color: inherit; - } - } - .content { - background-color: #fff; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/theme/styles.scss b/cla-frontend-contributor-console/src/ionic/theme/styles.scss deleted file mode 100644 index c310e8de0..000000000 --- a/cla-frontend-contributor-console/src/ionic/theme/styles.scss +++ /dev/null @@ -1,117 +0,0 @@ -@import './table'; -@import './modal'; -@import './footer'; - -ion-icon { - color: color($colors, gray); -} - -ion-icon[name='menu'] { - color: color($colors, white); -} - -.toolbar { - background-color: color($colors, white); - color: color($colors, gray); - padding: 4px 25px; - .bar-button { - color: inherit; - &:hover:not(.disable-hover) { - color: inherit; - } - } - .toolbar-background { - background-color: inherit; - } - .toolbar-title { - color: inherit; - } - .toolbar-content { - display: flex; - } -} - -.content { - background-color: color($colors, light); -} - -.grid { - max-width: 160rem; -} - -ion-content .scroll-content > header { - margin-top: -16px; - margin-right: -16px; - margin-left: -16px; - padding: 16px; - position: relative; - background-color: #fff; - &:after { - content: ''; - @extend .header-md::after; - } -} - -ion-menu { - .header { - &:after { - background-image: none; - border-top: 1px solid color($colors, dark); - } - .app-logo { - display: block; - margin: 2rem 2rem; - max-width: 150px; - } - } - - .menu-list { - padding-left: 2rem; - .item { - font-weight: bold; - } - .menu-list { - padding-bottom: 2rem; - .item { - font-weight: normal; - } - } - } - - .item-md { - &.item-block .item-inner { - border-bottom: 0; - } - } - - .scroll-content { - overflow-y: auto; - } -} - -.clickable { - cursor: pointer; -} - -.loading-display { - &-initial { - // display: none; - opacity: 0; - } - - &-loaded { - // display: block; - opacity: 1; - animation-name: loadingDisplayFadeIn; - animation-duration: 0.8s; - } -} - -@keyframes loadingDisplayFadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} diff --git a/cla-frontend-contributor-console/src/ionic/theme/table.scss b/cla-frontend-contributor-console/src/ionic/theme/table.scss deleted file mode 100644 index e32268d23..000000000 --- a/cla-frontend-contributor-console/src/ionic/theme/table.scss +++ /dev/null @@ -1,238 +0,0 @@ -// Tables -// -// ----------------- - -// Variables -// --------------------- -$table-header-font-weight: 600; -$table-header-font-color: #757575; - -$table-cell-padding: 1.6rem; -$table-condensed-cell-padding: $table-cell-padding/2; - -$table-bg: #fff; -$table-bg-accent: #f5f5f5; -$table-bg-hover: rgba(0, 0, 0, 0.12); -$table-bg-active: $table-bg-hover; -$table-border-color: #e0e0e0; - -// Baseline styles -.table { - width: 100%; - max-width: 100%; - margin-bottom: 2rem; - background-color: $table-bg; - > thead, - > tbody, - > tfoot { - > tr { - // .transition(all .3s ease); - > th, - > td { - text-align: left; - padding: $table-cell-padding; - vertical-align: top; - border-top: 0; - // .transition(all .3s ease); - } - } - } - > thead > tr > th { - font-weight: $table-header-font-weight; - color: $table-header-font-color; - vertical-align: bottom; - border-bottom: 1px solid rgba(0, 0, 0, 0.12); - } - > caption + thead, - > colgroup + thead, - > thead:first-child { - > tr:first-child { - > th, - > td { - border-top: 0; - } - } - } - > tbody + tbody { - border-top: 1px solid rgba(0, 0, 0, 0.12); - } - - // Nesting - .table { - background-color: $table-bg; - } - - // Remove border - .no-border { - border: 0; - } -} - -// Condensed table w/ half padding -.table-condensed { - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - padding: $table-condensed-cell-padding; - } - } - } -} - -// Bordered version -// -// Add horizontal borders between columns. -.table-bordered { - border: 0; - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - border: 0; - border-bottom: 1px solid $table-border-color; - } - } - } - > thead > tr { - > th, - > td { - border-bottom-width: 2px; - } - } -} - -// Zebra-striping -// -// Default zebra-stripe styles (alternating gray and transparent backgrounds) -.table-striped { - > tbody > tr:nth-child(odd) { - > td, - > th { - background-color: $table-bg-accent; - } - } -} - -// Hover effect -// -.table-hover { - > tbody > tr:hover { - > td, - > th { - background-color: $table-bg-hover; - } - } -} - -// Responsive tables (vertical) -// -// Wrap your tables in `.table-responsive-vertical` and we'll make them mobile friendly -// by vertical table-cell display. Only applies <768px. Everything above that will display normally. -// For correct display you must add 'data-title' to each 'td' -.table-responsive-vertical { - table { - border-collapse: collapse; - border-style: hidden; - } - tr { - background-color: #fafafa; - border: 1px solid #ddd; - border-width: 1px; - border-style: inset; - } - - @media screen and (max-width: 768px) { - // Tighten up spacing - > .table { - margin-bottom: 0; - background-color: transparent; - > thead, - > tfoot { - display: none; - } - - > tbody { - display: block; - - > tr { - display: block; - border: 1px solid $table-border-color; - border-radius: 2px; - margin-bottom: $table-cell-padding; - - > td { - background-color: $table-bg; - display: block; - vertical-align: middle; - text-align: right; - } - > td[data-title]:before { - content: attr(data-title); - float: left; - font-size: inherit; - font-weight: $table-header-font-weight; - color: $table-header-font-color; - } - } - } - } - - // Special overrides for shadows - &.shadow-z-1 { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - > .table > tbody > tr { - border: none; - // .shadow-z-1(); - } - } - - // Special overrides for the bordered tables - > .table-bordered { - border: 0; - - // Nuke the appropriate borders so that the parent can handle them - > tbody { - > tr { - > td { - border: 0; - border-bottom: 1px solid $table-border-color; - } - > td:last-child { - border-bottom: 0; - } - } - } - } - - // Special overrides for the striped tables - > .table-striped { - > tbody > tr > td, - > tbody > tr:nth-child(odd) { - background-color: $table-bg; - } - > tbody > tr > td:nth-child(odd) { - background-color: $table-bg-accent; - } - } - - // Special overrides for hover tables - > .table-hover { - > tbody { - > tr:hover > td, - > tr:hover { - background-color: $table-bg; - } - > tr > td:hover { - background-color: $table-bg-hover; - } - } - } - } -} diff --git a/cla-frontend-contributor-console/src/ionic/theme/variables.scss b/cla-frontend-contributor-console/src/ionic/theme/variables.scss deleted file mode 100755 index 897bb4059..000000000 --- a/cla-frontend-contributor-console/src/ionic/theme/variables.scss +++ /dev/null @@ -1,65 +0,0 @@ -// Ionic Variables and Theming. For more info, please see: -// http://ionicframework.com/docs/v2/theming/ -$font-path: '../assets/fonts'; - -@import 'ionic.globals'; - -// Shared Variables -// -------------------------------------------------- -// To customize the look and feel of this app, you can override -// the Sass variables found in Ionic's source scss files. -// To view all the possible Ionic variables, see: -// http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/ - -// Named Color Variables -// -------------------------------------------------- -// Named colors makes it easy to reuse colors on various components. -// It's highly recommended to change the default colors -// to match your app's branding. Ionic uses a Sass map of -// colors so you can add, rename and remove colors as needed. -// The "primary" color is the only required color in the map. - -$colors: ( - primary: #003764, - secondary: #00a0fc, - danger: #f53d3d, - light: #fafafa, - white: #ffffff, - dark: #222, - gray: gray, - green: green -); - -// App iOS Variables -// -------------------------------------------------- -// iOS only Sass variables can go here - -// App Material Design Variables -// -------------------------------------------------- -// Material Design only Sass variables can go here - -// App Windows Variables -// -------------------------------------------------- -// Windows only Sass variables can go here - -// App Theme -// -------------------------------------------------- -// Ionic apps can have different themes applied, which can -// then be future customized. This import comes last -// so that the above variables are used and Ionic's -// default are overridden. - -@import 'ionic.theme.default'; - -// Ionicons -// -------------------------------------------------- -// The premium icon font for Ionic. For more info, please see: -// http://ionicframework.com/docs/v2/ionicons/ - -@import 'ionic.ionicons'; - -// Fonts -// -------------------------------------------------- - -@import 'roboto'; -@import 'noto-sans'; diff --git a/cla-frontend-contributor-console/src/ionic/validators/calendarlink.ts b/cla-frontend-contributor-console/src/ionic/validators/calendarlink.ts deleted file mode 100644 index 062a5adfe..000000000 --- a/cla-frontend-contributor-console/src/ionic/validators/calendarlink.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright The Linux Foundation and each contributor to CommunityBridge. -// SPDX-License-Identifier: MIT - -import { FormControl } from '@angular/forms'; - -export class CalendarLinkValidator { - static isValid(control: FormControl): any { - let entered_url = control.value; - if (entered_url == null || entered_url == '') { - return null; - } - let calendar_url = 'https://calendar.google.com/calendar/embed'; - let calendar_embed = '