Separation of Duties in Code Commits with Your Pipeline

Editor’s Note: This is a guest blog post written by Ishu Gupta, senior software engineer at Capital One. The post first appeared on GitHub. 

Most of the pipelines with Jenkins today are built using Jenkins shared libraries. These shared libraries provide a huge amount of power to the users as well as to the administrators of these libs.

Jenkinsfiles are best when they are used for configuration and defining the stages of a pipeline only, while the real core logic of implementing those steps resides in a shared set of libraries.

DevOps WorldIf you are a pipeline admin in your team or company, you can define a lot of controls in your shared set of libraries and provide a huge flexibility to the users at the same time.

One of the common problems which is prevalent in the software development community is committing the bad/unwanted/malicious code in the git repository by surpassing the approval process. Even while a git repository can be secured by setting up the right controls in the git settings, a user with higher git privileges can always commit code intentionally or unintentionally to the release/master branch.

In such an event, shared libraries can be used to catch such commits, flag the build and take other actions to notify the user and his superior, for example. The team can take appropriate decisions like reverting the commit or fixing the permissions on their git repositories.

Here is an example how you can do it

  1. Wrap your Jenkinsfile in a parent closure. For example in this case integration is the parent closure:
  2. Since all the pipelines steps are now under integration closure, Before executing any pipeline steps, the code inside the integration closure will be executed. With this leverage, now you can apply all your admin rules under this closure.
  3. To implement commit validation functionality, in shared libs, in the integration block you can write this example code:
    stage('Validating the git commit'){
      def validateCommitWithPullRequest() {
          Map validationFlags = commitValidator(userIntiatedJob)
          boolean isValid = true
          // ignore user initiated job in case they need to rebuild due to error not related to their codebase
          if (validationFlags.pullViolation) {
              error "Validation failed: This change was not made through a pull request. This behavior is prohibited in Release branch."
          } else if (validationFlags.approverViolation) {
              error "Validation failed: The approver and requester of the pull request is same person. This behavior is prohibited in Release branch."
          } else if (validationFlags.noApproverViolation) {
              error "Validation failed: No approval was found for the pull request. This behavior is prohibited in Release branch."
          }
      }
      
      def commitValidator(boolean userIntiatedJob) {
          def hasResults = true
          int pageNum = 1
          def result = [pullViolation: true, approverViolation: false, noApproverViolation: false]
          while (hasResults) {
              String prListUrl = "https:///api/v3/repos///pulls?state=closed&sort=updated&direction=desc&per_page=30&page=${pageNum}"
              def prResponse = 
              if (hasResults) {
                  // Match the PR commit with the valid commit
                  def results = prResponse.find { pr -> pr.merge_commit_sha ==  }
                  if (results != null) {
                      result.pullViolation = false
                      def prRequester = results.user.login
                      // github approver check
                      def prUrl = results.url as String
                      def approverResponse = 
                      String approverData = approverResponse.getContent()
                      // find approved review by person other than requester
                      validGithubApproval = approverData.any { pr -> pr.user.login != prRequester && pr.state == 'APPROVED' }
                      // find approved review by requester
                      invalidGithubApproval = approverData.any { pr -> pr.user.login == prRequester && pr.state == 'APPROVED' }
                      break
                  }
              }
              pageNum++
          }
          return result
      }
    }
    

Result

Now, in the case of an invalid commit, you see the following on your pipeline page: 

And your console log shows a friend and informative error message:

The best part about these controls is that they force the discussion of doing the right thing. We have seen a lot of developers rectify the git settings properly after seeing such errors.

Summary

This is a very basic illustration of how you can use shared libraries to enforce controls. There are many more use cases that you can implement through using the admin closures in shared libraries. Such controls may not always completely address the issues at the core, but will help you in redefining the software development culture in your team/company. Be sure to catch my talk on Re-imagine your Pipeline: Deliver Software with Confidence at DevOps World | Jenkins World 2018.