package dehub import ( "bytes" "dehub/accessctl" "dehub/fs" "dehub/sigcred" "dehub/typeobj" "encoding" "encoding/base64" "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 // 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 accreddit 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 } func (r *Repo) assertAccessControls( accessCtls []accessctl.BranchAccessControl, creds []sigcred.Credential, branch plumbing.ReferenceName, 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 } matchRes, err := accessctl.Match(accessCtls, accessctl.MatchInteractions{ Branch: branch.Short(), FilePathsChanged: pathsChanged, }) if err != nil { return fmt.Errorf("could not determine applicable access controls: %w", err) } for _, matchedAC := range matchRes.ChangeAccessControls { ac := matchedAC.ChangeAccessControl condInt, err := ac.Condition.Interface() if err != nil { return fmt.Errorf("could not cast Condition of file path pattern %q to interface: %w", ac.FilePathPattern, err) } else if err := condInt.Satisfied(creds); err != nil { return fmt.Errorf("access control of file path pattern %q not satisfied: %w\nFiles matched:\n%s", ac.FilePathPattern, err, strings.Join(matchedAC.FilePaths, "\n")) } } 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 { commitObj, err := r.GitRepo.CommitObject(h) if err != nil { return fmt.Errorf("could not retrieve commit object: %w", err) } commitTree, err := r.GitRepo.TreeObject(commitObj.TreeHash) if err != nil { return fmt.Errorf("could not retrieve tree object: %w", err) } sigTree := commitTree // only for root commit parentTree := &object.Tree{} if commitObj.NumParents() > 0 { parent, err := commitObj.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) var commit Commit if err := commit.UnmarshalText([]byte(commitObj.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.Credentials, branch, parentTree, commitTree, ) 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(parentTree, 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 }