All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.GitEventHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016 Netflix, Inc.
 *
 * 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.
 */

package com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.echo.config.PipelineTriggerConfiguration;
import com.netflix.spinnaker.echo.model.Trigger;
import com.netflix.spinnaker.echo.model.trigger.GitEvent;
import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator;
import com.netflix.spinnaker.kork.artifacts.model.Artifact;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Implementation of TriggerEventHandler for events of type {@link GitEvent}, which occur when a
 * commit is pushed to a tracked git repository.
 */
@Component
@Slf4j
public class GitEventHandler extends BaseTriggerEventHandler {
  private static final String GIT_TRIGGER_TYPE = "git";
  private static final String GITHUB_SECURE_SIGNATURE_HEADER = "x-hub-signature";
  private static final List supportedTriggerTypes =
      Collections.singletonList(GIT_TRIGGER_TYPE);

  @Autowired private PipelineTriggerConfiguration pipelineTriggerConfiguration;

  @Autowired
  public GitEventHandler(
      Registry registry,
      ObjectMapper objectMapper,
      FiatPermissionEvaluator fiatPermissionEvaluator,
      PipelineTriggerConfiguration pipelineTriggerConfiguration) {
    super(registry, objectMapper, fiatPermissionEvaluator);
    this.pipelineTriggerConfiguration = pipelineTriggerConfiguration;
  }

  @Override
  public List supportedTriggerTypes() {
    return supportedTriggerTypes;
  }

  @Override
  public boolean handleEventType(String eventType) {
    return eventType.equalsIgnoreCase(GitEvent.TYPE);
  }

  @Override
  public Class getEventType() {
    return GitEvent.class;
  }

  @Override
  protected boolean isValidTrigger(Trigger trigger) {
    return trigger.isEnabled()
        && ((GIT_TRIGGER_TYPE.equals(trigger.getType())
            && trigger.getSource() != null
            && trigger.getProject() != null
            && trigger.getSlug() != null));
  }

  @Override
  public boolean isSuccessfulTriggerEvent(GitEvent event) {
    return true;
  }

  @Override
  protected Predicate matchTriggerFor(GitEvent gitEvent) {
    String source = gitEvent.getDetails().getSource();
    String project = gitEvent.getContent().getRepoProject();
    String slug = gitEvent.getContent().getSlug();
    String branch = gitEvent.getContent().getBranch();
    String action = gitEvent.getContent().getAction();

    return trigger ->
        trigger.getType().equals(GIT_TRIGGER_TYPE)
            && trigger.getSource().equalsIgnoreCase(source)
            && trigger.getProject().equalsIgnoreCase(project)
            && trigger.getSlug().equalsIgnoreCase(slug)
            && (trigger.getBranch() == null
                || trigger.getBranch().equals("")
                || matchesPattern(branch, trigger.getBranch()))
            && passesGithubAuthenticationCheck(gitEvent, trigger)
            && (trigger.getEvents() == null
                || trigger.getEvents().size() == 0
                || trigger.getEvents().stream().anyMatch(a -> a.equals(action)));
  }

  @Override
  protected List getArtifactsFromEvent(GitEvent gitEvent, Trigger trigger) {
    return gitEvent.getContent() != null && gitEvent.getContent().getArtifacts() != null
        ? gitEvent.getContent().getArtifacts()
        : new ArrayList<>();
  }

  @Override
  protected Function buildTrigger(GitEvent gitEvent) {
    return trigger -> {
      Trigger t =
          trigger
              .atHash(gitEvent.getHash())
              .atBranch(gitEvent.getBranch())
              .atEventId(gitEvent.getEventId())
              .atAction(gitEvent.getAction());

      return t.withProperties(
          Stream.concat(
                  Optional.ofNullable(t.getProperties())
                      .orElse(new HashMap<>())
                      .entrySet()
                      .stream(),
                  Stream.of(
                          new AbstractMap.SimpleEntry<>(
                              "number", gitEvent.getContent().getNumber()),
                          new AbstractMap.SimpleEntry<>("draft", gitEvent.getContent().getDraft()),
                          new AbstractMap.SimpleEntry<>("state", gitEvent.getContent().getState()),
                          new AbstractMap.SimpleEntry<>("title", gitEvent.getContent().getTitle()))
                      .filter(entry -> entry.getValue() != null))
              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
    };
  }

  private boolean matchesPattern(String s, String pattern) {
    Pattern p = Pattern.compile(pattern);
    Matcher m = p.matcher(s);
    return m.matches();
  }

  private boolean passesGithubAuthenticationCheck(GitEvent gitEvent, Trigger trigger) {
    boolean triggerHasSecret =
        StringUtils.isNotEmpty(trigger.getSecret())
            || StringUtils.isNotEmpty(pipelineTriggerConfiguration.getGitSharedSecret());
    boolean eventHasSignature =
        gitEvent.getDetails().getRequestHeaders().containsKey(GITHUB_SECURE_SIGNATURE_HEADER);

    if (triggerHasSecret && !eventHasSignature) {
      log.warn(
          "Received GitEvent from Github without secure signature for trigger configured with a secret");
      return false;
    }

    if (!triggerHasSecret && eventHasSignature) {
      log.warn(
          "Received GitEvent from Github with secure signature, but trigger did not contain the secret");
      return false;
    }
    // If we reach here, triggerHasSecret == eventHasSignature

    if (!triggerHasSecret) {
      // Trigger has no secret, and event sent no signature
      return true;
    }

    // Trigger has a secret, and event sent a signature
    return hasValidGitHubSecureSignature(gitEvent, trigger);
  }

  private boolean hasValidGitHubSecureSignature(GitEvent gitEvent, Trigger trigger) {
    String header =
        gitEvent.getDetails().getRequestHeaders().get(GITHUB_SECURE_SIGNATURE_HEADER).get(0);
    log.debug("GitHub Signature detected. " + GITHUB_SECURE_SIGNATURE_HEADER + ": " + header);
    String signature = StringUtils.removeStart(header, "sha1=");

    String secret = trigger.getSecret();
    if (StringUtils.isEmpty(secret)) {
      secret = pipelineTriggerConfiguration.getGitSharedSecret();
    }

    String computedDigest = HmacUtils.hmacSha1Hex(secret, gitEvent.getRawContent());

    // TODO: Find constant time comparison algo?
    boolean digestsMatch = signature.equalsIgnoreCase(computedDigest);
    if (!digestsMatch) {
      log.warn("Github Digest mismatch! Pipeline NOT triggered: " + trigger);
      log.debug("computedDigest: " + computedDigest + ", from GitHub: " + signature);
    }

    return digestsMatch;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy