gittuf/internal/attestations/github.go
Aditya Sirish A Yelgundhalli 4ccc102f8e
*: Use person associatedIdentities to verify code review tool approvals
Signed-off-by: Aditya Sirish A Yelgundhalli <ayelgundhall@bloomberg.net>
2024-11-07 17:21:19 -05:00

197 lines
8 KiB
Go

// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0
package attestations
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"path"
"github.com/gittuf/gittuf/internal/attestations/github"
githubv01 "github.com/gittuf/gittuf/internal/attestations/github/v01"
"github.com/gittuf/gittuf/internal/gitinterface"
sslibdsse "github.com/gittuf/gittuf/internal/third_party/go-securesystemslib/dsse"
gogithub "github.com/google/go-github/v61/github"
ita "github.com/in-toto/attestation/go/v1"
)
func NewGitHubPullRequestAttestation(owner, repository string, pullRequestNumber int, commitID string, pullRequest *gogithub.PullRequest) (*ita.Statement, error) {
return githubv01.NewPullRequestAttestation(owner, repository, pullRequestNumber, commitID, pullRequest)
}
func (a *Attestations) SetGitHubPullRequestAuthorization(repo *gitinterface.Repository, env *sslibdsse.Envelope, targetRefName, commitID string) error {
envBytes, err := json.Marshal(env)
if err != nil {
return err
}
blobID, err := repo.WriteBlob(envBytes)
if err != nil {
return err
}
if a.githubPullRequestAttestations == nil {
a.githubPullRequestAttestations = map[string]gitinterface.Hash{}
}
a.githubPullRequestAttestations[GitHubPullRequestAttestationPath(targetRefName, commitID)] = blobID
return nil
}
// GitHubPullRequestAttestationPath constructs the expected path on-disk for the
// GitHub pull request attestation.
func GitHubPullRequestAttestationPath(refName, commitID string) string {
return path.Join(refName, commitID)
}
// NewGitHubPullRequestApprovalAttestation creates a new GitHub pull request
// approval attestation for the provided information. The attestation is
// embedded in an in-toto "statement" and returned with the appropriate
// "predicate type" set. The `fromTargetID` and `toTargetID` specify the change
// to `targetRef` that is approved on the corresponding GitHub pull request.
func NewGitHubPullRequestApprovalAttestation(targetRef, fromRevisionID, targetTreeID string, approvers, dismissedApprovers []string) (*ita.Statement, error) {
return githubv01.NewPullRequestApprovalAttestation(targetRef, fromRevisionID, targetTreeID, approvers, dismissedApprovers)
}
// SetGitHubPullRequestApprovalAttestation writes the new GitHub pull request
// approval attestation to the object store and tracks it in the current
// attestations state. The refName, fromRevisionID, targetTreeID parameters are
// used to construct an indexPath. The hostURL and reviewID are together mapped
// to the indexPath so that if the review is dismissed later, the corresponding
// attestation can be updated.
func (a *Attestations) SetGitHubPullRequestApprovalAttestation(repo *gitinterface.Repository, env *sslibdsse.Envelope, hostURL string, reviewID int64, appName, refName, fromRevisionID, targetTreeID string) error {
// TODO: this will be updated to support validating different versions
if err := githubv01.ValidatePullRequestApproval(env, refName, fromRevisionID, targetTreeID); err != nil {
return errors.Join(github.ErrInvalidPullRequestApprovalAttestation, err)
}
envBytes, err := json.Marshal(env)
if err != nil {
return err
}
blobID, err := repo.WriteBlob(envBytes)
if err != nil {
return err
}
if a.codeReviewApprovalAttestations == nil {
a.codeReviewApprovalAttestations = map[string]gitinterface.Hash{}
}
if a.codeReviewApprovalIndex == nil {
a.codeReviewApprovalIndex = map[string]string{}
}
indexPath := GitHubPullRequestApprovalAttestationPath(refName, fromRevisionID, targetTreeID)
// We URL encode the appName to make it appropriate for an on-disk path
blobPath := path.Join(indexPath, base64.URLEncoding.EncodeToString([]byte(appName)))
// Note the distinction between indexPath and blobPath
// We don't have this for reference authorizations
// indexPath is of the form "<ref>/<from commit>-<target tree>/github"
// blobPath is a specific entry in the indexPath tree, for the app recording
// the attestation
a.codeReviewApprovalAttestations[blobPath] = blobID
githubReviewID, err := GitHubReviewID(hostURL, reviewID)
if err != nil {
return err
}
if existingIndexPath, has := a.codeReviewApprovalIndex[githubReviewID]; has {
if existingIndexPath != indexPath {
return github.ErrInvalidPullRequestApprovalAttestation
}
} else {
a.codeReviewApprovalIndex[githubReviewID] = indexPath // only use indexPath as the same review ID can be observed by more than one app
}
return nil
}
// GetGitHubPullRequestApprovalAttestationFor returns the requested GitHub pull
// request approval attestation. Here, all the pieces of information to load the
// attestation are known: the change the approval is for as well as the app that
// observed the approval.
func (a *Attestations) GetGitHubPullRequestApprovalAttestationFor(repo *gitinterface.Repository, appName, refName, fromRevisionID, targetTreeID string) (*sslibdsse.Envelope, error) {
indexPath := GitHubPullRequestApprovalAttestationPath(refName, fromRevisionID, targetTreeID)
return a.GetGitHubPullRequestApprovalAttestationForIndexPath(repo, appName, indexPath)
}
// GetGitHubPullRequestApprovalAttestationForReviewID returns the requested
// GitHub pull request approval attestation for the specified GitHub instance,
// review ID, and app. This is used when the indexPath is unknown, such as when
// dismissing a prior approval. The host information and reviewID are used to
// identify the indexPath for the requested review.
func (a *Attestations) GetGitHubPullRequestApprovalAttestationForReviewID(repo *gitinterface.Repository, hostURL string, reviewID int64, appName string) (*sslibdsse.Envelope, error) {
indexPath, has, err := a.GetGitHubPullRequestApprovalIndexPathForReviewID(hostURL, reviewID)
if err != nil {
return nil, err
}
if has {
return a.GetGitHubPullRequestApprovalAttestationForIndexPath(repo, appName, indexPath)
}
return nil, github.ErrGitHubReviewIDNotFound
}
// GetGitHubPullRequestApprovalAttestationForIndexPath returns the requested
// GitHub pull request approval attestation for the indexPath and appName.
func (a *Attestations) GetGitHubPullRequestApprovalAttestationForIndexPath(repo *gitinterface.Repository, appName, indexPath string) (*sslibdsse.Envelope, error) {
// We URL encode the appName to match the on-disk path
blobPath := path.Join(indexPath, base64.URLEncoding.EncodeToString([]byte(appName)))
blobID, has := a.codeReviewApprovalAttestations[blobPath]
if !has {
return nil, github.ErrPullRequestApprovalAttestationNotFound
}
envBytes, err := repo.ReadBlob(blobID)
if err != nil {
return nil, err
}
env := &sslibdsse.Envelope{}
if err := json.Unmarshal(envBytes, env); err != nil {
return nil, err
}
return env, nil
}
// GetGitHubPullRequestApprovalIndexPathForReviewID uses the host and review ID
// to find the previously recorded index path. Also see:
// SetGitHubPullRequestApprovalAttestation.
func (a *Attestations) GetGitHubPullRequestApprovalIndexPathForReviewID(hostURL string, reviewID int64) (string, bool, error) {
githubReviewID, err := GitHubReviewID(hostURL, reviewID)
if err != nil {
return "", false, err
}
indexPath, has := a.codeReviewApprovalIndex[githubReviewID]
return indexPath, has, nil
}
// GitHubPullRequestApprovalAttestationPath returns the expected path on-disk
// for the GitHub pull request approval attestation. This attestation type is
// stored using the same format as a reference authorization with the addition
// of `github` at the end of the path. This must be used as the tree to store
// specific attestation blobs in.
func GitHubPullRequestApprovalAttestationPath(refName, fromID, toID string) string {
return path.Join(ReferenceAuthorizationPath(refName, fromID, toID), githubPullRequestApprovalSystemName)
}
// GitHubReviewID converts a GitHub specific review ID (recorded as an int64
// number by GitHub) into a code review system agnostic identifier used by
// gittuf.
func GitHubReviewID(hostURL string, reviewID int64) (string, error) {
u, err := url.Parse(hostURL)
if err != nil {
return "", err
}
return fmt.Sprintf("%s::%d", u.Host, reviewID), nil
}