A read-only clone of the dehub project, for until dehub.dev can be brought back online.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dehub/commit.go

317 lines
9.8 KiB

package dehub
import (
"bytes"
"dehub/accessctl"
"dehub/fs"
"dehub/sigcred"
"dehub/typeobj"
"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"
)
// CommitInterface describes the methods which must be implemented by the
// different commit types.
type CommitInterface interface {
// MessageHead returns the head of the commit message (i.e. the first line).
MessageHead() (string, error)
// Hash returns the raw hash which Signifiers can sign to accredit this
// commit. The tree objects given describe the filesystem state of the
// parent commit, and the filesystem state of this commit.
//
// This method should _not_ change any fields on the commit.
Hash(parent, this *object.Tree) ([]byte, error)
// GetHash returns the signable Hash embedded in the commit, which should
// hopefully correspond to the Commit's Credentials.
GetHash() []byte
}
// Commit represents a single Commit which is being added to a branch. Only one
// field should be set on a Commit, unless otherwise noted.
type Commit struct {
Change *CommitChange `type:"change,default"`
Credential *CommitCredential `type:"credential"`
// Credentials represent all created Credentials for this commit, and can be
// set on all Commit objects regardless of other fields being set.
Credentials []sigcred.Credential `yaml:"credentials"`
}
// MarshalYAML implements the yaml.Marshaler interface.
func (c Commit) MarshalYAML() (interface{}, error) {
return typeobj.MarshalYAML(c)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *Commit) UnmarshalYAML(unmarshal func(interface{}) error) error {
return typeobj.UnmarshalYAML(c, unmarshal)
}
// Interface returns the CommitInterface instance encapsulated by this Commit
// object.
func (c Commit) Interface() (CommitInterface, error) {
el, _, err := typeobj.Element(c)
if err != nil {
return nil, err
}
return el.(CommitInterface), nil
}
// MarshalText implements the encoding.TextMarshaler interface by returning the
// form the Commit object takes in the git commit message.
func (c Commit) MarshalText() ([]byte, error) {
commitInt, err := c.Interface()
if err != nil {
return nil, fmt.Errorf("could not cast Commit %+v to interface : %w", c, err)
}
msgHead, err := commitInt.MessageHead()
if err != nil {
return nil, fmt.Errorf("error constructing message head: %w", err)
}
msgBodyB, err := yaml.Marshal(c)
if err != nil {
return nil, fmt.Errorf("error marshaling commit %+v as yaml: %w", c, err)
}
w := new(bytes.Buffer)
w.WriteString(msgHead)
w.WriteString("\n\n---\n")
w.Write(msgBodyB)
return w.Bytes(), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a
// Commit object which has been encoded into a git commit message.
func (c *Commit) UnmarshalText(msg []byte) error {
i := bytes.Index(msg, []byte("\n"))
if i < 0 {
return fmt.Errorf("commit message %q is malformed, it has no body", msg)
}
msgBody := msg[i:]
if err := yaml.Unmarshal(msgBody, c); err != nil {
return fmt.Errorf("could not unmarshal Commit message from yaml: %w", err)
}
return nil
}
// AccreditCommit returns the given Commit with an appended Credential provided
Modify how SignifierInterface is produced so it always sets AccountID on Credentials --- type: change message: |- Modify how SignifierInterface is produced so it always sets AccountID on Credentials Previously it was the responsibility of the caller of the Sign method to set the AccountID on the produced Credential, but this didn't really make sense. This commit makes it so that all SignifierInterface's produced by Signifier implicitly set the AccountID field. The solution here is still a bit hacky, and ultimately the real solution will probably be to refactor the structore of Credential, so that it doesn't have AccountID. change_hash: ADPuz04GuyxWwjo/0/jc7DcsPMl5rK0osSpaqmUxv818 credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5r+hgACgkQlcRvpqQRSKzwYBAAsY4tj+E5xtJSZ1TvrS0mwJ/lSHYWE4rS3eDMY3JUJLE1tr5k3OTRtUhh2UHCsArXSVF4sU8cBSCtf2noaThQm8KQghPMgoZ1LnPd4BnxxlE2gPik4FMcv+mCv9OgUh0AUO+rSXeYJA3oWunaW9kYollUdX/mVTQTmmbLBqBpeXF/TQO/bJTEEzA853j5QDT8//onfSIlzUw0UB57IZZZImp5/XrggHBbKdfhUTJ75LGMgDEDvDNIdV8lBys+RnMzK0Yj6EvLQhsw426+0Sf9vX3jtzj6WKhmi8QyYvcxIbcrWUScEfA/RAgf0A8KhqKq91bicSHjvyK1TZRSSWcS43ewamgvVWx0KSYYoIn7PPwOTmpHP8u6RzGEQFjOhP1EaGytQJKMXidU6CPTh+pYVtPZc8oLAwk+DyMquqfUSbzN/63t90HpTm7uycuzOnQxilYe2HKlbMJCId0a0DyAFrA+0pNRz0tyd3DvF4svCdEy82rzlUGEhq7aIJKoXIut+fKGEBd6Znz6oX15CyQq0oPthZcCqgFR0oTqufvV2iWo+26cd9dVTPVbJA9kSbaFchgdAqCkPA5wDVuNJJtMftf7STW8Lm6dnU6q9YFjZVdR55WtvUCINxBUtOirRzG1jcS0VNhhtb+SMNATEvDGJmt6neHM6Z17MAdwGS+s/hA= account: mediocregopher
4 years ago
// by the given SignifierInterface.
func (r *Repo) AccreditCommit(commit Commit, sigInt sigcred.SignifierInterface) (Commit, error) {
commitInt, err := commit.Interface()
if err != nil {
return commit, fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
}
headFS, err := r.HeadFS()
if err != nil {
return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err)
}
cred, err := sigInt.Sign(headFS, commitInt.GetHash())
if err != nil {
return commit, fmt.Errorf("could not accredit change commit: %w", err)
}
commit.Credentials = append(commit.Credentials, cred)
return commit, 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(),
},
})
}
// HasStagedChanges returns true if there are file changes which have been
// staged (e.g. via "git add").
func (r *Repo) HasStagedChanges() (bool, error) {
w, err := r.GitRepo.Worktree()
if err != nil {
return false, fmt.Errorf("error retrieving worktree: %w", err)
}
status, err := w.Status()
if err != nil {
return false, fmt.Errorf("error retrieving worktree status: %w", err)
}
var any bool
for _, fileStatus := range status {
if fileStatus.Staging != git.Unmodified {
any = true
break
}
}
return any, nil
}
type verificationCtx struct {
commit *object.Commit
commitTree, parentTree *object.Tree
isRootCommit bool
}
// non-gophers gonna hate on this method, but I say it's fine
func (r *Repo) verificationCtx(h plumbing.Hash) (vctx verificationCtx, err error) {
if vctx.commit, err = r.GitRepo.CommitObject(h); err != nil {
return vctx, fmt.Errorf("retrieving commit object: %w", err)
} else if vctx.commitTree, err = r.GitRepo.TreeObject(vctx.commit.TreeHash); err != nil {
return vctx, fmt.Errorf("retrieving commit tree object %q: %w",
vctx.commit.TreeHash, err)
} else if vctx.isRootCommit = vctx.commit.NumParents() == 0; vctx.isRootCommit {
vctx.parentTree = new(object.Tree)
} else if parent, err := vctx.commit.Parent(0); err != nil {
return vctx, fmt.Errorf("retrieving commit parent: %w", err)
} else if vctx.parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil {
return vctx, fmt.Errorf("retrieving commit parent tree object %q: %w",
parent.Hash, err)
}
return vctx, nil
}
func (r *Repo) assertAccessControls(
accessCtls []accessctl.BranchAccessControl,
commit Commit, vctx verificationCtx, branch plumbing.ReferenceName,
) (err error) {
filesChanged, err := calcDiff(vctx.parentTree, vctx.commitTree)
if err != nil {
return fmt.Errorf("calculating diff from tree %q to tree %q: %w",
vctx.parentTree.Hash, vctx.commitTree.Hash, err)
} else if len(filesChanged) > 0 && commit.Change == nil {
return errors.New("files changes but commit is not a change commit")
}
pathsChanged := make([]string, len(filesChanged))
for i := range filesChanged {
pathsChanged[i] = filesChanged[i].path
}
Refactor access controls to support multiple branches message: |- Refactor access controls to support multiple branches This was a big lift. It implements a backwards incompatible change to overhaul access control patterns to also encompass which branch is being interacted with, not only which files. The `accessctl` package was significantly rewritten to support this, as well as some of the code modifying it. The INTRODUCTION and SPEC were also appropriately updated. The change to the SPEC is _technically_ backwards incompatible, but it won't effect anything. The `access_control` previously being used will just be ignored, and the changes to `accessctl` include the definition of fallback access controls which will automatically be applied if nothing else matches, so when verifying the older history of this repo those will be used. change_hash: AIfNYLmOLGpuyTiVodT3hDe9lF4E+5DbOTgSdkbjJONb credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5aw0sACgkQlcRvpqQRSKy7kw//UMyS/waV/tE1vntZrMbmEtFmiXPcMVNal76cjhdiF3He50qXoWG6m0qWz+arD1tbjoZml6pvU+Xt45y/Uc54DZizzz0E9azoFW0/uvZiLApftFRArZbT9GhbDs2afalyoTJx/xvQu+a2FD/zfljEWE8Zix+bwHCLojiYHHVA65HFLEt8RsH33jFyzWvS9a2yYqZXL0qrU9tdV68hazdIm1LCp+lyVV74TjwxPAZDOmNAE9l4EjIk1pgr2Qo4u2KwJqCGdVCvka8TiFFYiP7C6319ZhDMyj4m9yZsd1xGtBd9zABVBDgmzCEjt0LI3Tv35lPd2tpFBkjQy0WGcMAhwSHWSP7lxukQMCEB7og/SwtKaExiBJhf1HRO6H9MlhNSW4X1xwUgP+739ixKKUY/RcyXgZ4pkzt6sewAMVbUOcmzXdUvuyDJQ0nhDFowgicpSh1m8tTkN1aLUx18NfnGZRgkgBeE6EpT5/+NBfFwvpiQkXZ3bcMiNhNTU/UnWMyqjKlog+8Ca/7CqgswYppMaw4iPaC54H8P6JTH+XnqDlLKSkvh7PiJJa5nFDG07fqc8lYKm1KGv6virAhEsz/AYKLoNGIsqXt+mYUDLvQpjlRsiN52euxyn5e41LcrH0RidIGMVeaS+7re1pWbkCkMMMtYlnCbC5L6mfrBu6doN8o= account: mediocregopher
4 years ago
matchRes, err := accessctl.Match(accessCtls, accessctl.MatchInteractions{
Branch: branch.Short(),
FilePathsChanged: pathsChanged,
CredentialAdded: commit.Credential != nil,
Refactor access controls to support multiple branches message: |- Refactor access controls to support multiple branches This was a big lift. It implements a backwards incompatible change to overhaul access control patterns to also encompass which branch is being interacted with, not only which files. The `accessctl` package was significantly rewritten to support this, as well as some of the code modifying it. The INTRODUCTION and SPEC were also appropriately updated. The change to the SPEC is _technically_ backwards incompatible, but it won't effect anything. The `access_control` previously being used will just be ignored, and the changes to `accessctl` include the definition of fallback access controls which will automatically be applied if nothing else matches, so when verifying the older history of this repo those will be used. change_hash: AIfNYLmOLGpuyTiVodT3hDe9lF4E+5DbOTgSdkbjJONb credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5aw0sACgkQlcRvpqQRSKy7kw//UMyS/waV/tE1vntZrMbmEtFmiXPcMVNal76cjhdiF3He50qXoWG6m0qWz+arD1tbjoZml6pvU+Xt45y/Uc54DZizzz0E9azoFW0/uvZiLApftFRArZbT9GhbDs2afalyoTJx/xvQu+a2FD/zfljEWE8Zix+bwHCLojiYHHVA65HFLEt8RsH33jFyzWvS9a2yYqZXL0qrU9tdV68hazdIm1LCp+lyVV74TjwxPAZDOmNAE9l4EjIk1pgr2Qo4u2KwJqCGdVCvka8TiFFYiP7C6319ZhDMyj4m9yZsd1xGtBd9zABVBDgmzCEjt0LI3Tv35lPd2tpFBkjQy0WGcMAhwSHWSP7lxukQMCEB7og/SwtKaExiBJhf1HRO6H9MlhNSW4X1xwUgP+739ixKKUY/RcyXgZ4pkzt6sewAMVbUOcmzXdUvuyDJQ0nhDFowgicpSh1m8tTkN1aLUx18NfnGZRgkgBeE6EpT5/+NBfFwvpiQkXZ3bcMiNhNTU/UnWMyqjKlog+8Ca/7CqgswYppMaw4iPaC54H8P6JTH+XnqDlLKSkvh7PiJJa5nFDG07fqc8lYKm1KGv6virAhEsz/AYKLoNGIsqXt+mYUDLvQpjlRsiN52euxyn5e41LcrH0RidIGMVeaS+7re1pWbkCkMMMtYlnCbC5L6mfrBu6doN8o= account: mediocregopher
4 years ago
})
if err != nil {
return fmt.Errorf("determining applicable access controls: %w", err)
}
defer func() {
if err != nil {
err = fmt.Errorf("asserting access controls for branch_pattern %q: %w",
matchRes.BranchPattern, err)
}
}()
for _, matchedChangeAC := range matchRes.ChangeAccessControls {
ac := matchedChangeAC.ChangeAccessControl
if condInt, err := ac.Condition.Interface(); err != nil {
return fmt.Errorf("casting ChangeAccessControl.Condition %#v to interface: %w", ac.Condition, err)
} else if err := condInt.Satisfied(commit.Credentials); err != nil {
return fmt.Errorf("satisfying change_access_control with file_path_pattern %q: %w\nfiles matched:\n%s",
ac.FilePathPattern, err, strings.Join(matchedChangeAC.FilePaths, "\n"))
}
}
if matchRes.CredentialAccessControl != nil {
cond := matchRes.CredentialAccessControl.CredentialAccessControl.Condition
if condInt, err := cond.Interface(); err != nil {
return fmt.Errorf("casting CredentialAccessControl.Condition %#v to interface: %w", cond, err)
} else if err := condInt.Satisfied(commit.Credentials); err != nil {
return fmt.Errorf("satisfying credential_access_control: %w", err)
}
}
return nil
}
// VerifyCommit verifies that the commit at the given hash, which is presumably
// on the given branch, is gucci.
func (r *Repo) VerifyCommit(branch plumbing.ReferenceName, h plumbing.Hash) error {
vctx, err := r.verificationCtx(h)
if err != nil {
return err
}
var sigFS fs.FS
if vctx.isRootCommit {
sigFS = fs.FromTree(vctx.commitTree)
} else {
sigFS = fs.FromTree(vctx.parentTree)
}
var commit Commit
if err := commit.UnmarshalText([]byte(vctx.commit.Message)); err != nil {
return err
}
cfg, err := r.loadConfig(sigFS)
if err != nil {
return fmt.Errorf("error loading config: %w", err)
}
err = r.assertAccessControls(cfg.AccessControls, commit, vctx, branch)
if err != nil {
return fmt.Errorf("failed to satisfy all access controls: %w", err)
}
commitInt, err := commit.Interface()
if err != nil {
return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
}
changeHash := commitInt.GetHash()
expectedChangeHash, err := commitInt.Hash(vctx.parentTree, vctx.commitTree)
if err != nil {
return fmt.Errorf("error calculating expected change hash: %w", err)
} else if !bytes.Equal(changeHash, expectedChangeHash) {
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
base64.StdEncoding.EncodeToString(expectedChangeHash),
base64.StdEncoding.EncodeToString(changeHash))
}
for _, cred := range commit.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
}