dehub/commit.go
mediocregopher a5bee27892 Change all references to 'master' into 'trunk'
message: Change all references to 'master' into 'trunk'
change_hash: ABJwxLvHMmj63oJPIv/vNeRCIp1ZDZYuPLQT57x9K1lO
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5Je88ACgkQlcRvpqQRSKyxyw/7B51/vvtlxLan9Z6q6rh7uyGcFf6WpWGiRDIccAJHqzegGP4eAb8V4Jzi9H4JJ82TnAc+EUegs8ewRiOcWj6YkU463b5CgUSwnzYKm86K6SyHGW1WH9OxFIDEMzCaVsktMEc9iLMl0dJNzakhPzu+qy74pU5xlypjCBzRLFeuqmnf4M1fq4FAq6fCs7ZVB3LccyC0mhCWsS2eiuCE/mVQ7WROVpxj5tplp71jlX6ZtWU7qsgvQS2V8ggtTVpCT2WE4u6bnu1oYOpSs9g+sxKKOAHKvZfjAMLG9qM3pOl1J+44W2Ms/mtON0VUX7G1Q5XVcmM0hPopXsiWLiAslSOAOuL+LE5iLq4nz1RyIbVh0QakYr+4NJL6Yt0L2I6lNVnUS4SgmMr86n8ZCcCjDAs6g5d7Zchqp3S3EF3bbJuLi0ICoOCxTD2gNkjo2BGverI8APLTUpujQl+9W/sGmT2aEdTpruGYjIcwsRaGo8VMDFECdZply5Ng1FbBoohv+j3vdO05fRyNkvRu3CBxP2tUe39jLxUmpu62igGF2VjZptsK0bLdzDpQ5Hv6jSZjn+mPyFJ5w+EX2JCRYpjn2eaYYtI/IyULGTaftzcEZeZibEhd/wQJnsEJNqXpCilDKgZajZyYhgy7BSeT9xDf2d8qyFoqWpvFhshHcesbUfG2O+Y=
  account: mediocregopher
2020-02-16 10:28:59 -07:00

246 lines
7.1 KiB
Go

package dehub
import (
"bytes"
"dehub/accessctl"
"dehub/fs"
"dehub/sigcred"
"dehub/yamlutil"
"encoding"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
yaml "gopkg.in/yaml.v2"
)
// TrunkCommit describes the structure of the object encoded into the git
// message of a commit in the repo trunk.
type TrunkCommit struct {
Message string `yaml:"message"`
ChangeHash yamlutil.Blob `yaml:"change_hash"`
Credentials []sigcred.Credential `yaml:"credentials"`
}
type tcYAML struct {
Val TrunkCommit `yaml:",inline"`
}
func msgHead(msg string) string {
i := strings.Index(msg, "\n")
if i > 0 {
return msg[:i]
}
return msg
}
// MarshalText implements the encoding.TextMarshaler interface by returning the
// form the TrunkCommit object takes in the git commit message.
func (tc TrunkCommit) MarshalText() ([]byte, error) {
trunkCommitEncoded, err := yaml.Marshal(tcYAML{tc})
if err != nil {
return nil, fmt.Errorf("failed to encode TrunkCommit message: %w", err)
}
fullMsg := msgHead(tc.Message) + "\n\n" + string(trunkCommitEncoded)
return []byte(fullMsg), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a
// TrunkCommit object which has been encoded into a git commit message.
func (tc *TrunkCommit) UnmarshalText(msg []byte) error {
i := bytes.Index(msg, []byte("\n"))
if i < 0 {
return fmt.Errorf("commit message %q is malformed", msg)
}
msgHead, msg := msg[:i], msg[i:]
var tcy tcYAML
if err := yaml.Unmarshal(msg, &tcy); err != nil {
return fmt.Errorf("could not unmarshal TrunkCommit message: %w", err)
}
*tc = tcy.Val
if !strings.HasPrefix(tc.Message, string(msgHead)) {
return errors.New("encoded TrunkCommit is malformed, it might not be an encoded TrunkCommit")
}
return nil
}
// Commit uses the given TextMarshaler to create a git commit object (with the
// specified accountID as the author) and commits it to the current HEAD,
// returning the hash of the commit.
func (r *Repo) Commit(m encoding.TextMarshaler, accountID string) (plumbing.Hash, error) {
msgB, err := m.MarshalText()
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("error marshaling %T to string: %v", m, err)
}
w, err := r.GitRepo.Worktree()
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("could not get git worktree: %w", err)
}
return w.Commit(string(msgB), &git.CommitOptions{
Author: &object.Signature{
Name: accountID,
When: time.Now(),
},
})
}
// NewTrunkCommit constructs a TrunkCommit using the given SignifierInterface to
// create a Credential for it.
func (r *Repo) NewTrunkCommit(msg, accountID string, sig sigcred.SignifierInterface) (TrunkCommit, error) {
_, headTree, err := r.head()
if errors.Is(err, plumbing.ErrReferenceNotFound) {
headTree = &object.Tree{}
} else if err != nil {
return TrunkCommit{}, err
}
_, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo)
if err != nil {
return TrunkCommit{}, err
}
// this is necessarily different than headTree for the case of there being
// no HEAD (ie it's the first commit). In that case we want headTree to be
// empty (because it's being used to generate the change hash), but we want
// the signifier to use the raw fs (because that's where the signifier's
// data might be).
sigFS, err := r.headOrRawFS()
if err != nil {
return TrunkCommit{}, err
}
cfg, err := r.loadConfig(sigFS)
if err != nil {
return TrunkCommit{}, fmt.Errorf("could not load config: %w", err)
}
changeHash := genChangeHash(nil, msg, headTree, stagedTree)
cred, err := sig.Sign(sigFS, changeHash)
if err != nil {
return TrunkCommit{}, fmt.Errorf("failed to sign commit hash: %w", err)
}
cred.AccountID = accountID
// This isn't strictly necessary, but we want to save people the effort of
// creating an invalid commit, pushing it, having it be rejected, then
// having to reset on the commit.
err = r.assertAccessControls(
cfg.AccessControls, []sigcred.Credential{cred},
headTree, stagedTree,
)
if err != nil {
return TrunkCommit{}, fmt.Errorf("commit would not satisfy access controls: %w", err)
}
return TrunkCommit{
Message: msg,
ChangeHash: changeHash,
Credentials: []sigcred.Credential{cred},
}, nil
}
func (r *Repo) assertAccessControls(
accessCtls []accessctl.AccessControl, creds []sigcred.Credential,
from, to *object.Tree,
) error {
filesChanged, err := calcDiff(from, to)
if err != nil {
return err
}
pathsChanged := make([]string, len(filesChanged))
for i := range filesChanged {
pathsChanged[i] = filesChanged[i].path
}
accessCtls, err = accessctl.ApplicableAccessControls(accessCtls, pathsChanged)
if err != nil {
return fmt.Errorf("could not determine applicable access controls: %w", err)
}
for _, accessCtl := range accessCtls {
condInt, err := accessCtl.Condition.Interface()
if err != nil {
return fmt.Errorf("could not cast Condition to interface: %w", err)
} else if err := condInt.Satisfied(creds); err != nil {
return fmt.Errorf("access control for pattern %q not satisfied: %w",
accessCtl.Pattern, err)
}
}
return nil
}
// VerifyTrunkCommit verifies that the commit at the given hash, which is
// presumably on the repo trunk, is gucci.
func (r *Repo) VerifyTrunkCommit(h plumbing.Hash) error {
commit, err := r.GitRepo.CommitObject(h)
if err != nil {
return fmt.Errorf("could not retrieve commit object: %w", err)
}
commitTree, err := r.GitRepo.TreeObject(commit.TreeHash)
if err != nil {
return fmt.Errorf("could not retrieve tree object: %w", err)
}
var trunkCommit TrunkCommit
if err := trunkCommit.UnmarshalText([]byte(commit.Message)); err != nil {
return err
}
sigTree := commitTree // only for root commit
parentTree := &object.Tree{}
if commit.NumParents() > 0 {
parent, err := commit.Parent(0)
if err != nil {
return fmt.Errorf("could not retrieve parent of commit: %w", err)
} else if parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil {
return fmt.Errorf("could not retrieve tree object of parent %q: %w", parent.Hash, err)
}
sigTree = parentTree
}
sigFS := fs.FromTree(sigTree)
cfg, err := r.loadConfig(sigFS)
if err != nil {
return fmt.Errorf("error loading config: %w", err)
}
err = r.assertAccessControls(
cfg.AccessControls, trunkCommit.Credentials,
parentTree, commitTree,
)
if err != nil {
return fmt.Errorf("failed to satisfy all access controls: %w", err)
}
expectedChangeHash := genChangeHash(nil, trunkCommit.Message, parentTree, commitTree)
if !bytes.Equal(trunkCommit.ChangeHash, expectedChangeHash) {
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
base64.StdEncoding.EncodeToString(expectedChangeHash),
base64.StdEncoding.EncodeToString(trunkCommit.ChangeHash))
}
for _, cred := range trunkCommit.Credentials {
sig, err := r.signifierForCredential(sigFS, cred)
if err != nil {
return fmt.Errorf("error finding signifier for credential %+v: %w", cred, err)
} else if err := sig.Verify(sigFS, expectedChangeHash, cred); err != nil {
return fmt.Errorf("error verifying credential %+v: %w", cred, err)
}
}
return nil
}