Only allow Bitbucket users of a specific group to approve a job

Only allow Bitbucket users of a specific group to approve a job

Any user in the organization can approve a type: approval job, however it is possible to control the execution of any downstream job that requires the type: approval job.

To do so, we generally recommend using restricted context, with the method outlined in our documentation. However, due to a limitation of the Bitbucket API, it is not possible to restrict CircleCI contexts for Bitbucket projects.

This workaround will allow you to specify a Bitbucket group of users for whom a workflow can continue after they approved a type: approval job.

How it works

  1. Retrieves the ID, then the full name of the user who approved the job
  2. Queries the Bitbucket API endpoint /1.0/groups to check if that user is a member of the specified group
    • If the user is a member of the group, the workflow continues.
    • If the user isn’t a member, the whole workflow is cancelled before any other job can be executed.

Requirements

  1. To make calls to the Bitbucket API you will need to create an App password for an Admin user of the related Bitbucket workspace.
  2. To successfully send authenticated requests to CircleCI API v2, you must use a personal API token (project tokens are currently not supported on API v2)

Step 1 – Add all required environment variables to your project settings

Go to your project settings and add the following environment variables:

Variable Name Description
CCI_Token This will be a personal API token.
BB_admin_username A user with admin permissions on the Bitbucket workspace.
BB_admin_App_Password An App password for the above user
APPROVERS_GRP The Bitbucket group containing users who are allowed to approve the job

 
Note:

  • The user for which the CircleCI personal API token is generated doesn’t have to (but can) be the same user as BB_admin_username.
  • If you prefer not to use a CircleCI personal API token or a Bitbucket App password generated for your own account (or any real user’s account), you can create a machine-user with admin access to the Bitbucket workspace, and generate both the CircleCI personal API token the Bitbucket App password for that machine-user

Step 2 – Add the check_approve.sh script to the project repository

Create a check_approve.sh file at the root of your repository with the below content:

Note that you can create the file anywhere in your repository. However, if you don’t add it to the root of your repository you’ll need to specify the path to the script when referencing it in your config.yml (see Step 3)

#!/bin/bash

### Retrieve the CircleCI user ID of the job approver
WHO_APPROVED_ID=$(curl -X GET https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}/job -H "Circle-Token: ${CCI_Token}" | jq -r '.items[]|select(.type == "approval")|.approved_by')

### Use the ID to get the job approver's full name
WHO_APPROVED_FULL_NAME=$(curl -X GET https://circleci.com/api/v2/user/${WHO_APPROVED_ID} -H "Circle-Token: ${CCI_Token}" | jq -r .name)

ACCESS_GRANTED=0

### Check if the job approver is in the Bitbucket group
GRP_MBRS=$(curl -u ${BB_admin_username}:${BB_admin_App_Password} -G "https://api.bitbucket.org/1.0/groups/${CIRCLE_PROJECT_USERNAME}/${APPROVERS_GRP}/members"|jq -r '.[].display_name')
IFS=$'\n'
for OUTPUT in $GRP_MBRS; do
    if [[ $OUTPUT == "${WHO_APPROVED_FULL_NAME}" ]]; then
    ACCESS_GRANTED=1
    fi
done

### If the job approver IS NOT in the Bitbucket group, cancel the whole workflow.
if [[ $ACCESS_GRANTED -eq 0 ]]; then
  echo "You are not authorized to approve this job. Cancelling workflow..."
  curl -X POST https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}/cancel -H "Circle-Token: ${CCI_Token}"
### If the job approver IS in the Bitbucket group, continue the workflow execution.
  else
    echo "You are authorized to approve this job. Continuing workflow..."
fi

### Allow sufficient time to complete the cancel operation (if applicable)
sleep 3

Step 3 – Include check_approve.sh in your project’s config.yml

You can then use the check_approve.sh script as in the below example:

version: 2.1

jobs:
  first:
    docker:
      - image: circleci/node
    steps:
      - run:
          command: |
                   echo job1

  second:
    docker:
      - image: circleci/node
    steps:
      - run:
          command: |
                   echo job2

  approval-access-check:
    docker:
      - image: circleci/node
    steps:
      - checkout
      - run:
          command: |
            chmod +x ./check_approve.sh
            ./check_approve.sh

workflows:
  version: 2
  papa:
    jobs:
      - first
      - wait-for-approval:
          type: approval
          requires:
            - first
      - approval-access-check:              
          requires:
            - wait-for-approval
      - second:
          requires:
            - approval-access-check

 
Alternatively, you could leverage the pre-steps functionality instead of using an additional script:

version: 2.1

jobs:
  first:
    docker:
      - image: circleci/node
    steps:
      - run:
          command: |
                   echo job1

  second:
    docker:
      - image: circleci/node
    steps:
      - run:
          command: |
                   echo job2

workflows:
  version: 2
  example:
    jobs:
      - first
      - wait-for-approval:
          type: approval
          requires:
            - first
      - second:
          pre-steps: # steps to run before steps defined in the job bar
            - run:
                command: |
                    WHO_APPROVED_ID=$(curl -X GET https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}/job -H "Circle-Token: ${CCI_Token}" | jq -r '.items[]|select(.type == "approval")|.approved_by')
                    WHO_APPROVED_FULL_NAME=$(curl -X GET https://circleci.com/api/v2/user/${WHO_APPROVED_ID} -H "Circle-Token: ${CCI_Token}" | jq -r .name)
                    ACCESS_GRANTED=false
                    GRP_MBRS=$(curl -s -u ${BB_admin_username}:${BB_admin_App_Password} --request GET "https://api.bitbucket.org/1.0/groups/truckman3/${APPROVERS_GRP}/members"|jq -r '.[].display_name')
                    IFS=$'\n'
                    for OUTPUT in $GRP_MBRS; do
                      if [[ $OUTPUT == "${WHO_APPROVED_FULL_NAME}" ]]; then
                       ACCESS_GRANTED=1
                      fi
                    done

                    if [[ $ACCESS_GRANTED -eq 0 ]]; then
                      echo "You are not authorized to approve this job. Cancelling workflow..."
                      curl -X POST https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}/cancel -H "Circle-Token: ${CCI_Token}"
                      else
                        echo "You are authorized to approve this job. Continuing workflow..."
                    fi

                     sleep 3
          requires:
            - wait-for-approval

Caveats

  • The purpose of this workaround is to prevent “accidental” job approvals; it does not provide any security functionality.
  • From a security point of view, this method is not a substitute for restricted contexts; Any user with sufficient access to rerun the build with SSH would be able to print out the value of all the above environment variables.

Resources

Please consider adding your vote to this open feature request regarding context security for Bitbucket projects.

4 Likes