a5bee27892
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
246 lines
7.1 KiB
Go
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
|
|
}
|