mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-03 11:17:08 +00:00
197 lines
8 KiB
Go
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
|
|
}
|